init release

This commit is contained in:
Andreas Knuth 2024-11-17 11:56:55 +01:00
parent 435753880c
commit 21cde45999
18 changed files with 1023 additions and 157 deletions

View File

@ -86,6 +86,9 @@
}, },
"serve": { "serve": {
"builder": "@angular-devkit/build-angular:dev-server", "builder": "@angular-devkit/build-angular:dev-server",
"options": {
"proxyConfig": "src/proxy.conf.json"
},
"configurations": { "configurations": {
"production": { "production": {
"buildTarget": "vokabeltraining:build:production" "buildTarget": "vokabeltraining:build:production"

746
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,6 +17,7 @@
"@angular/platform-browser": "^18.2.0", "@angular/platform-browser": "^18.2.0",
"@angular/platform-browser-dynamic": "^18.2.0", "@angular/platform-browser-dynamic": "^18.2.0",
"@angular/router": "^18.2.0", "@angular/router": "^18.2.0",
"flowbite": "^2.5.2",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"zone.js": "~0.14.10" "zone.js": "~0.14.10"
@ -25,6 +26,7 @@
"@angular-devkit/build-angular": "^18.2.12", "@angular-devkit/build-angular": "^18.2.12",
"@angular/cli": "^18.2.12", "@angular/cli": "^18.2.12",
"@angular/compiler-cli": "^18.2.0", "@angular/compiler-cli": "^18.2.0",
"tailwindcss": "^3.4.15",
"typescript": "~5.5.2" "typescript": "~5.5.2"
} }
} }

View File

@ -1,17 +1,23 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router'; import { RouterOutlet } from '@angular/router';
import { initFlowbite } from 'flowbite';
import { DeckListComponent } from './deck-list.component';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
template: ` template: `
<h1>Welcome to {{title}}!</h1> <div class="container mx-auto p-4">
<h1 class="text-3xl font-bold text-center mb-8">Vokabeltraining</h1>
<router-outlet /> <app-deck-list></app-deck-list>
</div>
`, `,
styles: [], standalone: true,
imports: [ CommonModule, DeckListComponent]
}) })
export class AppComponent { export class AppComponent {
title = 'vokabeltraining'; title = 'vokabeltraining';
ngOnInit(): void {
initFlowbite();
}
} }

View File

@ -2,7 +2,8 @@ import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router'; import { provideRouter } from '@angular/router';
import { routes } from './app.routes'; import { routes } from './app.routes';
import { provideHttpClient } from '@angular/common/http';
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes)] providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes),provideHttpClient()]
}; };

View File

@ -0,0 +1,26 @@
<!-- src/app/create-deck-modal.component.html -->
<div id="createDeckModal" tabindex="-1" aria-hidden="true" class="fixed top-0 left-0 right-0 z-50 hidden w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-modal md:h-full">
<div class="relative w-full h-full max-w-md md:h-auto">
<div class="relative bg-white rounded-lg shadow">
<button type="button" class="absolute top-3 right-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center" data-modal-hide="createDeckModal">
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
<span class="sr-only">Schließen</span>
</button>
<div class="p-6">
<h3 class="mb-4 text-xl font-medium text-gray-900">Neues Deck erstellen</h3>
<form (submit)="createDeck($event)">
<div class="mb-4">
<label for="deckName" class="block text-sm font-medium text-gray-700">Deck-Name</label>
<input type="text" id="deckName" [(ngModel)]="deckName" name="deckName" required class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" />
</div>
<button type="submit" class="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600">
Erstellen
</button>
</form>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,44 @@
// src/app/create-deck-modal.component.ts
import { Component, Output, EventEmitter } from '@angular/core';
import { DeckService } from '../deck.service';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Modal } from 'flowbite';
@Component({
selector: 'app-create-deck-modal',
templateUrl: './create-deck-modal.component.html',
standalone: true,
imports: [CommonModule,FormsModule]
})
export class CreateDeckModalComponent {
@Output() deckCreated = new EventEmitter<void>();
deckName: string = '';
constructor(private deckService: DeckService) { }
createDeck(event: Event): void {
event.preventDefault();
if (this.deckName.trim() === '') {
alert('Bitte einen Deck-Namen eingeben.');
return;
}
this.deckService.createDeck(this.deckName).subscribe({
next: () => {
this.deckName = '';
this.deckCreated.emit();
// Modal schließen
const modalElement = document.getElementById('createDeckModal');
if (modalElement) {
const modal = new Modal(modalElement);
modal.hide();
}
},
error: (err) => {
console.error('Fehler beim Erstellen des Decks', err);
alert('Fehler beim Erstellen des Decks.');
}
});
}
}

View File

@ -0,0 +1,43 @@
<!-- src/app/deck-list.component.html -->
<div>
<!-- Button zum Erstellen eines neuen Decks -->
<div class="flex justify-end mb-4">
<button data-modal-target="createDeckModal" data-modal-toggle="createDeckModal" class="bg-green-500 text-white py-2 px-4 rounded hover:bg-green-600">
Neues Deck erstellen
</button>
</div>
<!-- Decks anzeigen -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div *ngFor="let deck of decks" class="bg-white shadow rounded-lg p-6 flex flex-col">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-semibold">{{ deck.name }}</h2>
<button (click)="deleteDeck(deck.name)" class="text-red-500 hover:text-red-700">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="flex-1 mb-4">
<p class="text-gray-600">{{ deck.images.length }} Bilder</p>
</div>
<div class="flex space-x-2">
<button (click)="openTraining(deck)" class="flex-1 bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600">
Training starten
</button>
<button [attr.data-modal-target]="'uploadImageModal'" [attr.data-modal-toggle]="'uploadImageModal'" [attr.data-deck-name]="deck.name" class="flex-1 bg-green-500 text-white py-2 px-4 rounded hover:bg-green-600">
Bild hinzufügen
</button>
</div>
<!-- UploadImageModalComponent wird mit deck.name als Input geöffnet -->
<app-upload-image-modal [deckName]="deck.name" (imageUploaded)="loadDecks()"></app-upload-image-modal>
</div>
</div>
<!-- CreateDeckModalComponent -->
<app-create-deck-modal (deckCreated)="loadDecks()"></app-create-deck-modal>
<!-- TrainingComponent -->
<app-training *ngIf="selectedDeck" [deck]="selectedDeck" (close)="closeTraining()"></app-training>
</div>

View File

@ -0,0 +1,58 @@
// src/app/deck-list.component.ts
import { Component, OnInit } from '@angular/core';
import { DeckService, Deck } from './deck.service';
import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { CreateDeckModalComponent } from './create-deck-modal/create-deck-modal.component';
import { TrainingComponent } from './training/training.component';
import { UploadImageModalComponent } from './upload-image-modal/upload-image-modal.component';
import { initFlowbite } from 'flowbite';
@Component({
selector: 'app-deck-list',
templateUrl: './deck-list.component.html',
standalone: true,
imports: [
CommonModule,
CreateDeckModalComponent,
UploadImageModalComponent,
TrainingComponent
]
})
export class DeckListComponent implements OnInit {
decks: Deck[] = [];
selectedDeck: Deck | null = null;
constructor(private deckService: DeckService) { }
ngOnInit(): void {
this.loadDecks();
}
loadDecks(): void {
this.deckService.getDecks().subscribe({
next: (data) => this.decks = data,
error: (err) => console.error('Fehler beim Laden der Decks', err)
});
}
deleteDeck(deckName: string): void {
if (!confirm(`Bist du sicher, dass du das Deck "${deckName}" löschen möchtest?`)) {
return;
}
this.deckService.deleteDeck(deckName).subscribe({
next: () => this.loadDecks(),
error: (err) => console.error('Fehler beim Löschen des Decks', err)
});
}
openTraining(deck: Deck): void {
this.selectedDeck = deck;
}
closeTraining(): void {
this.selectedDeck = null;
this.loadDecks();
}
}

42
src/app/deck.service.ts Normal file
View File

@ -0,0 +1,42 @@
// src/app/deck.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface Deck {
name: string;
images: DeckImage[];
}
export interface DeckImage {
image: string;
text: string;
}
@Injectable({
providedIn: 'root'
})
export class DeckService {
private apiUrl = '/api/decks';
constructor(private http: HttpClient) { }
getDecks(): Observable<Deck[]> {
return this.http.get<Deck[]>(this.apiUrl);
}
createDeck(deckname: string): Observable<any> {
return this.http.post(this.apiUrl, { deckname });
}
deleteDeck(deckName: string): Observable<any> {
return this.http.delete(`${this.apiUrl}/${encodeURIComponent(deckName)}`);
}
uploadImage(deckName: string, image: File, text: string): Observable<any> {
const formData = new FormData();
formData.append('image', image);
formData.append('text', text);
return this.http.post(`${this.apiUrl}/${encodeURIComponent(deckName)}/images`, formData);
}
}

View File

@ -0,0 +1,24 @@
<!-- src/app/training.component.html -->
<div class="mt-10">
<h2 class="text-2xl font-bold mb-4">Training: {{ deck.name }}</h2>
<div class="bg-white shadow rounded-lg p-6 flex flex-col items-center">
<img [src]="currentImage?.image" alt="Vokabelbild" class="w-64 h-64 object-contain mb-4">
<div *ngIf="showTextFlag" class="bg-black bg-opacity-50 text-white text-lg p-2 rounded mb-4">
{{ currentImage?.text }}
</div>
<div class="flex space-x-4">
<button (click)="showText()" class="bg-green-500 text-white py-2 px-4 rounded hover:bg-green-600" [disabled]="showTextFlag">
Anzeigen
</button>
<button (click)="markKnown()" class="bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600">
Gewusst
</button>
<button (click)="markUnknown()" class="bg-red-500 text-white py-2 px-4 rounded hover:bg-red-600">
Nicht gewusst
</button>
</div>
<p class="mt-4">{{ progress }}</p>
<button (click)="closeTraining()" class="mt-4 text-gray-500 hover:text-gray-700 underline">Training beenden</button>
</div>
</div>

View File

@ -0,0 +1,62 @@
// src/app/training.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Deck, DeckImage } from '../deck.service';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-training',
templateUrl: './training.component.html',
standalone: true,
imports: [CommonModule]
})
export class TrainingComponent {
@Input() deck!: Deck;
@Output() close = new EventEmitter<void>();
currentIndex: number = 0;
knownCount: number = 0;
unknownCount: number = 0;
showTextFlag: boolean = false;
get currentImage(): DeckImage | null {
if (this.currentIndex < this.deck.images.length) {
return this.deck.images[this.currentIndex];
}
return null;
}
get progress(): string {
return `Fortschritt: ${this.currentIndex} / ${this.deck.images.length}`;
}
showText(): void {
this.showTextFlag = true;
}
markKnown(): void {
this.knownCount++;
this.nextImage();
}
markUnknown(): void {
this.unknownCount++;
this.nextImage();
}
nextImage(): void {
this.currentIndex++;
this.showTextFlag = false;
if (this.currentIndex >= this.deck.images.length) {
this.endTraining();
}
}
endTraining(): void {
alert(`Training beendet!\nGewusst: ${this.knownCount}\nNicht gewusst: ${this.unknownCount}`);
this.close.emit();
}
closeTraining(): void {
this.close.emit();
}
}

View File

@ -0,0 +1,31 @@
<!-- src/app/upload-image-modal.component.html -->
<div id="uploadImageModal" tabindex="-1" aria-hidden="true" class="fixed top-0 left-0 right-0 z-50 hidden w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-modal md:h-full">
<div class="relative w-full h-full max-w-md md:h-auto">
<div class="relative bg-white rounded-lg shadow">
<button type="button" class="absolute top-3 right-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center" data-modal-hide="uploadImageModal">
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
<span class="sr-only">Schließen</span>
</button>
<div class="p-6">
<h3 class="mb-4 text-xl font-medium text-gray-900">Bild zu Deck hinzufügen</h3>
<form (submit)="uploadImage($event)">
<div class="mb-4">
<label for="imageFile" class="block text-sm font-medium text-gray-700">Bild hochladen</label>
<input type="file" id="imageFile" (change)="onFileChange($event)" accept="image/*" required class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" />
</div>
<div class="mb-4">
<label for="imageText" class="block text-sm font-medium text-gray-700">Text zur Vokabel</label>
<input type="text" id="imageText" [(ngModel)]="imageText" name="imageText" required class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" />
</div>
<input type="hidden" [value]="deckName" />
<button type="submit" class="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600">
Hochladen
</button>
</form>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,54 @@
// src/app/upload-image-modal.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { DeckService } from '../deck.service';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Modal } from 'flowbite';
@Component({
selector: 'app-upload-image-modal',
templateUrl: './upload-image-modal.component.html',
standalone: true,
imports: [CommonModule,FormsModule]
})
export class UploadImageModalComponent {
@Input() deckName: string = '';
@Output() imageUploaded = new EventEmitter<void>();
imageFile: File | null = null;
imageText: string = '';
constructor(private deckService: DeckService) { }
onFileChange(event: Event): void {
const input = event.target as HTMLInputElement;
if (input.files && input.files.length) {
this.imageFile = input.files[0];
}
}
uploadImage(event: Event): void {
event.preventDefault();
if (!this.imageFile || this.imageText.trim() === '') {
alert('Bitte ein Bild und den zugehörigen Text angeben.');
return;
}
this.deckService.uploadImage(this.deckName, this.imageFile, this.imageText).subscribe({
next: () => {
this.imageFile = null;
this.imageText = '';
this.imageUploaded.emit();
// Modal schließen
const modalElement = document.getElementById('uploadImageModal');
if (modalElement) {
const modal = new Modal(modalElement);
modal.hide();
}
},
error: (err) => {
console.error('Fehler beim Hochladen des Bildes', err);
alert('Fehler beim Hochladen des Bildes.');
}
});
}
}

6
src/proxy.conf.json Normal file
View File

@ -0,0 +1,6 @@
{
"/api": {
"target": "http://localhost:5000",
"secure": false
}
}

View File

@ -1 +1,4 @@
/* You can add global styles to this file, and also import other style files */ /* You can add global styles to this file, and also import other style files */
@tailwind base;
@tailwind components;
@tailwind utilities;

13
tailwind.config.js Normal file
View File

@ -0,0 +1,13 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{html,ts}",
"./node_modules/flowbite/**/*.js" // add this line
],
theme: {
extend: {},
},
plugins: [
require('flowbite/plugin')
],
}

View File

@ -1,5 +1,3 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{ {
"compileOnSave": false, "compileOnSave": false,
"compilerOptions": { "compilerOptions": {