Feature: Create Event - First Step Frontend #17

Merged
igorpropisnov merged 20 commits from feature/create-event into main 2024-08-22 14:58:36 +02:00
7 changed files with 287 additions and 68 deletions
Showing only changes of commit 0314855a0d - Show all commits

View File

@ -1,6 +1,25 @@
<div class="flex flex-col h-full">
<div class="flex-grow overflow-y-auto px-8 py-8">
<div class="w-full flex flex-col">
<div class="w-full bg-neutral max-w-full sticky top-0 z-20 pt-2">
<ul class="steps steps-horizontal w-full py-2">
@for (step of steps; track $index) {
<li
[class.step-primary]="$index === currentStep()"
[class.step-success]="$index < currentStep() && isStepValid($index)"
[class.step-warning]="$index < currentStep() && !isStepValid($index)"
[attr.data-content]="getStepContent($index)"
class="step text-sm md:text-base"
(click)="goToStep($index)"
[class.cursor-pointer]="
$index <= currentStep() || canAdvanceToStep($index)
">
<span>{{ step }}</span>
</li>
}
</ul>
</div>
<div class="flex-grow overflow-y-auto px-4 sm:px-8 py-8">
<div class="w-full max-w-4xl mx-auto">
<div class="">
@if (currentStep() === 0) {
<app-basic-step></app-basic-step>
@ -10,23 +29,34 @@
</div>
<div
[ngStyle]="actionbar"
class="w-full max-w-full bg-primary sticky bottom-0 z-50">
<!-- <button type="button" class="btn btn-primary" (click)="prevStep()">
Prev
</button>
<button type="button" class="btn btn-primary" (click)="nextStep()">
Next
</button> -->
<ul class="steps steps-horizontal w-full">
@for (step of steps; track $index) {
<li
[class.step-accent]="$index <= currentStep()"
class="step text-sm md:text-base">
<span>{{ step }}</span>
</li>
class="w-full bg-neutral max-w-full sticky bottom-0 z-20 px-4 sm:px-8 py-4">
<div class="flex justify-between max-w-4xl mx-auto">
<button
type="button"
class="btn btn-primary"
(click)="prevStep()"
[disabled]="currentStep() === 0"
[class.opacity-50]="currentStep() === 0">
@if (currentStep() > 0) {
<span>
Back to
<span class="font-bold">{{ steps[currentStep() - 1] }}</span>
</span>
}
</ul>
</button>
<button
type="button"
class="btn btn-primary"
(click)="nextStep()"
[disabled]="currentStep() === steps.length - 1"
[class.opacity-50]="currentStep() === steps.length - 1">
@if (currentStep() < steps.length - 1) {
<span>
Next to
<span class="font-bold">{{ steps[currentStep() + 1] }}</span>
</span>
}
</button>
</div>
</div>
</div>

View File

@ -4,15 +4,9 @@ import {
ChangeDetectionStrategy,
signal,
WritableSignal,
ElementRef,
OnInit,
} from '@angular/core';
import {
BackgroundPatternService,
ThemeService,
} from '../../../shared/service';
import { BasicStepComponent } from './steps/basic-step.component';
@Component({
@ -24,42 +18,51 @@ import { BasicStepComponent } from './steps/basic-step.component';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CreateEventComponent implements OnInit {
public actionbar: { 'background-image': string } | null = null;
public currentStep: WritableSignal<number> = signal(0);
public readonly steps: string[] = ['Basic', 'Tickets', 'Review'];
public constructor(
private readonly backgroundPatternService: BackgroundPatternService,
private readonly themeService: ThemeService,
private readonly el: ElementRef
) {}
public constructor() {}
public ngOnInit(): void {
this.setBackground();
}
public ngOnInit(): void {}
public setBackground(): void {
const theme = this.themeService.getTheme();
let opacity: number;
if (theme === 'dark') {
opacity = 0.05;
public getStepContent(index: number): string {
if (index < this.currentStep()) {
return this.isStepValid(index) ? '✓' : '?';
} else {
opacity = 0.1;
return (index + 1).toString();
}
}
const colorPrimaryC = getComputedStyle(
this.el.nativeElement
).getPropertyValue('--pc');
public goToStep(stepIndex: number): void {
if (stepIndex < this.currentStep()) {
this.currentStep.set(stepIndex);
} else if (this.canAdvanceToStep(stepIndex)) {
this.currentStep.set(stepIndex);
}
}
const svgUrlActionbar = this.backgroundPatternService.getBankNotePattern(
colorPrimaryC,
opacity
);
public canAdvanceToStep(stepIndex: number): boolean {
for (let i = this.currentStep(); i < stepIndex; i++) {
if (!this.isStepValid(i)) {
return false;
}
}
return true;
}
this.actionbar = {
'background-image': `url("${svgUrlActionbar}")`,
};
public isStepValid(stepIndex: number): boolean {
// TODO: Implementiere die tatsächliche Validierungslogik hier
// Für Demonstrationszwecke nehmen wir an, dass der erste Schritt immer gültig ist,
// und die anderen zufällig gültig oder ungültig sind
return true;
// In Zukunft könnte es so aussehen:
// switch(stepIndex) {
// case 0: return this.isBasicStepValid();
// case 1: return this.isTicketsStepValid();
// case 2: return this.isReviewStepValid();
// default: return false;
// }
}
public nextStep(): void {

View File

@ -1,18 +1,33 @@
<form class="flex flex-col">
<h2 class="text-2xl font-bold mb-6">Event Basic Information</h2>
<h1 class="text-2xl font-bold mb-6">Event Basic Information</h1>
<div class="form-control mb-4">
<label class="label" for="event-title">
<label class="form-control w-full mb-4">
<div class="label">
<span class="label-text">Event Title</span>
</label>
<span class="label-text-alt">Make it catchy and descriptive</span>
</div>
<input
type="text"
id="event-title"
placeholder="Enter your event title"
class="input input-bordered w-full" />
<div class="label">
<span class="label-text-alt"></span>
<span class="label-text-alt"></span>
</div>
</label>
<div class="form-control w-full mb-4">
<app-dropdown
label="Event Location"
placeholder="Search for a Location"
hintTopRight="Select or create a new Location by using the dropdown"
dividerText="Or add a new Location"
newItemText="Create new Location"
emptyStateText="No matching Locations found"
[items]="items"></app-dropdown>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<!-- <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div class="form-control">
<label class="label" for="event-date">
<span class="label-text">Date</span>
@ -25,8 +40,8 @@
</label>
<input type="time" id="event-time" class="input input-bordered" />
</div>
</div>
</div> -->
<!--
<div class="form-control mb-4">
<label class="label" for="location-title">
<span class="label-text">Location</span>
@ -36,9 +51,9 @@
id="location-title"
placeholder="Enter your event title"
class="input input-bordered w-full" />
</div>
</div> -->
<div class="form-control mb-4">
<!-- <div class="form-control mb-4">
<label class="label" for="event-street">
<span class="label-text">Street</span>
</label>
@ -47,9 +62,9 @@
id="event-street"
placeholder="Street name"
class="input input-bordered" />
</div>
</div> -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<!-- <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div class="form-control">
<label class="label" for="event-number">
<span class="label-text">Street Number</span>
@ -70,9 +85,9 @@
placeholder="Postal code"
class="input input-bordered" />
</div>
</div>
</div> -->
<div class="form-control mb-6">
<!-- <div class="form-control mb-6">
<label class="label" for="event-city">
<span class="label-text">City</span>
</label>
@ -81,5 +96,5 @@
id="event-city"
placeholder="City"
class="input input-bordered" />
</div>
</div> -->
</form>

View File

@ -1,13 +1,19 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { DropdownComponent } from '../../../../shared/components/dropdown/dropdown.component';
@Component({
selector: 'app-basic-step',
standalone: true,
templateUrl: './basic-step.component.html',
providers: [],
imports: [],
imports: [CommonModule, FormsModule, DropdownComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BasicStepComponent {
public items: string[] = ['Option 1', 'Option 2', 'Option 3'];
public constructor() {}
}

View File

@ -0,0 +1,95 @@
<div class="form-control w-full mb-4">
<div class="label">
<span class="label-text">{{ label() }}</span>
<span class="label-text-alt">
{{ hintTopRight() }}
</span>
</div>
<div class="relative">
<label class="input input-bordered flex items-center gap-2">
<input
type="text"
[placeholder]="placeholder()"
class="grow"
(focus)="onFocus()"
(blur)="onBlur()"
(input)="onInput($event)"
[(ngModel)]="searchTerm" />
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6"
[class.rotate-180]="showDropdown()">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg>
</label>
@if (showDropdown()) {
<ul
class="menu bg-base-100 w-full mt-1 shadow-lg rounded-box absolute top-full left-0 z-10">
@if (filteredItems().length > 0) {
@for (item of filteredItems(); track item) {
<li>
<a
(mousedown)="selectItem(item)"
class="flex justify-between items-center"
[class.font-bold]="item === selectedItem()"
[class.text-primary]="item === selectedItem()">
<span>{{ item }}</span>
@if (item === selectedItem()) {
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m4.5 12.75 6 6 9-13.5" />
</svg>
}
</a>
</li>
}
<div class="divider">{{ dividerText() }}</div>
} @else if (searchTerm()) {
<li
(mousedown)="submitNewItem()"
class="text-center py-2 text-gray-500">
{{ emptyStateText() }}
</li>
}
<li>
<a
(mousedown)="submitNewItem()"
class="flex items-center text-primary">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6 mr-2">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 4.5v15m7.5-7.5h-15" />
</svg>
{{ newItemText() }}
</a>
</li>
</ul>
}
</div>
<div class="label">
<span class="label-text-alt">{{ hintBottomLeft() }}</span>
<span class="label-text-alt">{{ hintBottomRight() }}</span>
</div>
</div>

View File

@ -0,0 +1,69 @@
import { CommonModule } from '@angular/common';
import {
Component,
input,
InputSignal,
output,
OutputEmitterRef,
signal,
WritableSignal,
} from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-dropdown',
templateUrl: './dropdown.component.html',
standalone: true,
imports: [CommonModule, FormsModule],
})
export class DropdownComponent {
public label: InputSignal<string> = input.required<string>();
public placeholder: InputSignal<string> = input.required<string>();
public items: InputSignal<string[]> = input.required<string[]>();
public emptyStateText: InputSignal<string> = input<string>('');
public dividerText: InputSignal<string> = input<string>('');
public newItemText: InputSignal<string> = input<string>('');
public required: InputSignal<boolean> = input<boolean>(false);
public hintTopRight: InputSignal<string> = input<string>('');
public hintBottomLeft: InputSignal<string> = input<string>('');
public hintBottomRight: InputSignal<string> = input<string>('');
public itemSelected: OutputEmitterRef<string> = output<string>();
public searchTerm: WritableSignal<string> = signal('');
public showDropdown: WritableSignal<boolean> = signal<boolean>(false);
public filteredItems: WritableSignal<string[]> = signal<string[]>([]);
public selectedItem: WritableSignal<string | null> = signal<string | null>(
null
);
public onInput(event: Event): void {
const value = (event.target as HTMLInputElement).value;
this.searchTerm.set(value);
this.filteredItems.set(
this.items().filter((item) =>
item.toLowerCase().includes(value.toLowerCase())
)
);
this.showDropdown.set(true);
}
public selectItem(item: string): void {
this.searchTerm.set(item);
this.selectedItem.set(item);
this.showDropdown.set(false);
this.itemSelected.emit(item);
}
public submitNewItem(): void {}
public onFocus(): void {
this.showDropdown.set(true);
this.filteredItems.set(this.items());
}
public onBlur(): void {
setTimeout(() => {
this.showDropdown.set(false);
}, 200);
}
}

View File

@ -0,0 +1 @@
export * from './dropdown/dropdown.component';