Compare commits

..

5 Commits

Author SHA1 Message Date
Roland Schneider
c169faf288 add e2e 2025-10-30 21:02:05 +01:00
Schneider Roland
b599e79273 add e2e with add groups 2025-10-30 03:51:50 +01:00
Schneider Roland
45a69eea8a add e2e with add groups 2025-10-29 22:56:11 +01:00
Roland Schneider
2f54770720 add e2e 2025-10-29 16:30:17 +01:00
Schneider Roland
bdf16a3189 add e2e with docker-compose support 2025-10-29 08:12:23 +01:00
379 changed files with 2071 additions and 23833 deletions

6
.env.e2e Normal file
View File

@ -0,0 +1,6 @@
DATABASE_USER=test
DATABASE_PASS=test
DATABASE_HOST=localhost
DATABASE_NAME=test
DATABASE_PORT=4401
JWT_SECRET="secret"

14
.gitignore vendored
View File

@ -1,7 +1,7 @@
# compiled output
dist
node_modules
build
/dist
/node_modules
/build
# Logs
logs
@ -16,11 +16,11 @@ lerna-debug.log*
.DS_Store
# Tests
coverage
.nyc_output
/coverage
/.nyc_output
# IDEs and editors
.idea
/.idea
.project
.classpath
.c9/
@ -54,5 +54,3 @@ pids
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
tmp

View File

@ -1,9 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="all:start:dev" type="CompoundRunConfigurationType">
<toRun name="dev:gui" type="ShConfigurationType" />
<toRun name="dev:lib" type="ShConfigurationType" />
<toRun name="dev:server" type="ShConfigurationType" />
<toRun name="docker-env-dev" type="ShConfigurationType" />
<method v="2" />
</configuration>
</component>

View File

@ -1,17 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="dev:gui" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="cd admin &amp;&amp; npm run start" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="/usr/bin/zsh" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<envs />
<method v="2" />
</configuration>
</component>

View File

@ -1,17 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="dev:lib" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="cd admin &amp;&amp; ng build @rschneider/ng-daisyui --watch " />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="/usr/bin/zsh" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<envs />
<method v="2" />
</configuration>
</component>

View File

@ -1,17 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="dev:server" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="cd server &amp;&amp; npm run start" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="/usr/bin/zsh" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<envs />
<method v="2" />
</configuration>
</component>

View File

@ -1,17 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="docker-env-dev" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="cd environment/dev &amp;&amp; docker compose up -d" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="/usr/bin/zsh" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<envs />
<method v="2" />
</configuration>
</component>

View File

@ -1,17 +0,0 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
ij_typescript_use_double_quotes = false
[*.md]
max_line_length = off
trim_trailing_whitespace = false

43
admin/.gitignore vendored
View File

@ -1,43 +0,0 @@
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
__screenshots__/
# System files
.DS_Store
Thumbs.db

View File

@ -1,5 +0,0 @@
{
"plugins": {
"@tailwindcss/postcss": {}
}
}

View File

@ -1,4 +0,0 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
"recommendations": ["angular.ng-template"]
}

View File

@ -1,20 +0,0 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "ng serve",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: start",
"url": "http://localhost:4200/"
},
{
"name": "ng test",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: test",
"url": "http://localhost:9876/debug.html"
}
]
}

View File

@ -1,42 +0,0 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
},
{
"type": "npm",
"script": "test",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
}
]
}

View File

@ -1,59 +0,0 @@
# Admin
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.3.9.
## Development server
To start a local development server, run:
```bash
ng serve
```
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
## Code scaffolding
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
```bash
ng generate component component-name
```
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
```bash
ng generate --help
```
## Building
To build the project run:
```bash
ng build
```
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
## Running unit tests
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
```bash
ng test
```
## Running end-to-end tests
For end-to-end (e2e) testing, run:
```bash
ng e2e
```
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
## Additional Resources
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.

View File

@ -1,123 +0,0 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"admin": {
"projectType": "application",
"schematics": {},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"browser": "src/main.ts",
"tsConfig": "tsconfig.app.json",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.css"
]
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumError": "8kB"
}
],
"outputHashing": "all",
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.production.ts"
}
]
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.development.ts"
}
]
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular/build:dev-server",
"configurations": {
"production": {
"buildTarget": "admin:build:production"
},
"development": {
"buildTarget": "admin:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n"
},
"test": {
"builder": "@angular/build:karma",
"options": {
"tsConfig": "tsconfig.spec.json",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.css"
]
}
}
}
},
"@rschneider/ng-daisyui": {
"projectType": "library",
"root": "projects/rschneider/ng-daisyui",
"sourceRoot": "projects/rschneider/ng-daisyui/src",
"prefix": "rs-daisy",
"architect": {
"build": {
"builder": "@angular/build:ng-packagr",
"configurations": {
"production": {
"tsConfig": "projects/rschneider/ng-daisyui/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "projects/rschneider/ng-daisyui/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular/build:karma",
"options": {
"tsConfig": "projects/rschneider/ng-daisyui/tsconfig.spec.json"
}
}
}
}
}
}

10763
admin/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,60 +0,0 @@
{
"name": "admin",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve --proxy-config proxy.conf.json",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"prettier": {
"printWidth": 100,
"singleQuote": true,
"overrides": [
{
"files": "*.html",
"options": {
"parser": "angular"
}
}
]
},
"private": true,
"dependencies": {
"@angular/common": "^20.3.0",
"@angular/compiler": "^20.3.0",
"@angular/core": "^20.3.0",
"@angular/forms": "^20.3.0",
"@angular/platform-browser": "^20.3.0",
"@angular/router": "^20.3.0",
"@fullcalendar/angular": "^6.1.19",
"@fullcalendar/core": "^6.1.19",
"@fullcalendar/daygrid": "^6.1.19",
"@fullcalendar/interaction": "^6.1.19",
"@fullcalendar/list": "^6.1.19",
"@fullcalendar/timegrid": "^6.1.19",
"@tailwindcss/postcss": "^4.1.17",
"daisyui": "^5.4.5",
"date-fns": "^4.1.0",
"jwt-decode": "^4.0.0",
"postcss": "^8.5.6",
"rxjs": "~7.8.0",
"tailwindcss": "^4.1.17",
"tslib": "^2.3.0"
},
"devDependencies": {
"@angular/build": "^20.3.9",
"@angular/cli": "^20.3.9",
"@angular/compiler-cli": "^20.3.0",
"@types/jasmine": "~5.1.0",
"jasmine-core": "~5.9.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"ng-packagr": "^20.3.0",
"typescript": "~5.9.2"
}
}

View File

@ -1,63 +0,0 @@
# NgDaisyui
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.3.0.
## Code scaffolding
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
```bash
ng generate component component-name
```
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
```bash
ng generate --help
```
## Building
To build the library, run:
```bash
ng build ng-daisyui
```
This command will compile your project, and the build artifacts will be placed in the `dist/` directory.
### Publishing the Library
Once the project is built, you can publish your library by following these steps:
1. Navigate to the `dist` directory:
```bash
cd dist/ng-daisyui
```
2. Run the `npm publish` command to publish your library to the npm registry:
```bash
npm publish
```
## Running unit tests
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
```bash
ng test
```
## Running end-to-end tests
For end-to-end (e2e) testing, run:
```bash
ng e2e
```
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
## Additional Resources
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.

View File

@ -1,7 +0,0 @@
{
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../../dist/rschneider/ng-daisyui",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View File

@ -1,12 +0,0 @@
{
"name": "@rschneider/ng-daisyui",
"version": "0.0.1",
"peerDependencies": {
"@angular/common": "^20.3.0",
"@angular/core": "^20.3.0"
},
"dependencies": {
"tslib": "^2.3.0"
},
"sideEffects": false
}

View File

@ -1,26 +0,0 @@
@if (breadcrumbs()?.length) {
<div class="breadcrumbs text-sm">
<ul>
@for (breadcrumb of breadcrumbs(); track breadcrumb; ) {
<li>
@if (breadcrumb.url) {
<a [routerLink]="breadcrumb.url">
@if (breadcrumb.showIcon !== false) {
<span [outerHTML]="(breadcrumb.svgIcon || defaultLinkIcon) | safeHtml"></span>
}
{{ breadcrumb.text }}
</a>
} @else {
<span class="inline-flex items-center gap-2">
@if (breadcrumb.showIcon !== false) {
<span [outerHTML]="(breadcrumb.svgIcon || defaultIcon) | safeHtml"></span>
}
{{ breadcrumb.text }}
</span>
}
</li>
}
</ul>
</div>
}

View File

@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Breadcrumbs } from './breadcrumbs';
describe('Breadcrumbs', () => {
let component: Breadcrumbs;
let fixture: ComponentFixture<Breadcrumbs>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Breadcrumbs]
})
.compileComponents();
fixture = TestBed.createComponent(Breadcrumbs);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,46 +0,0 @@
import { Component, input } from '@angular/core';
import { Breadcrumb } from '../../daisy.types';
import { RouterLink } from '@angular/router';
import { SafeHtmlPipe } from '../../pipes/safe-html-pipe';
@Component({
selector: 'rs-daisy-breadcrumbs',
imports: [
RouterLink,
SafeHtmlPipe,
],
templateUrl: './breadcrumbs.html',
styleUrl: './breadcrumbs.css',
})
export class Breadcrumbs {
breadcrumbs = input<Breadcrumb[]>([]);
defaultLinkIcon: string = `<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="h-4 w-4 stroke-current">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path>
</svg>
`;
defaultIcon: string = `
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="h-4 w-4 stroke-current">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
`;
}

View File

@ -1,38 +0,0 @@
<button
[aria-disabled]="disabled()"
role="button"
[ngClass]="{
'btn': true,
'btn-primary': variant() === 'primary',
'btn-secondary': variant() === 'secondary',
'btn-accent': variant() === 'accent',
'btn-info': variant() === 'info',
'btn-success': variant() === 'success',
'btn-warning': variant() === 'warning',
'btn-error': variant() === 'error',
'btn-ghost': variant() === 'ghost',
'btn-link': variant() === 'link',
'btn-lg': size() === 'lg',
'btn-md': size() === 'md',
'btn-sm': size() === 'sm',
'btn-xs': size() === 'xs',
'btn-outline': outline(),
'btn-active': active(),
'btn-glass': glass(),
'btn-square': square(),
'btn-circle': circle(),
'btn-wide': wide(),
'btn-block': block(),
'no-animation': noAnimation(),
'loading': loading(),
'btn-responsive': responsive(),
'btn-disabled': disabled()
}"
[class]="customClass() || ''"
(click)="onClick($event)"
>
<ng-content />
</button>

View File

@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Button } from './button';
describe('Button', () => {
let component: Button;
let fixture: ComponentFixture<Button>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Button]
})
.compileComponents();
fixture = TestBed.createComponent(Button);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,60 +0,0 @@
// rs-daisy-button.component.ts
import { Component, input, HostBinding, output } from '@angular/core';
import { NgClass } from '@angular/common';
@Component({
selector: 'rs-daisy-button',
standalone: true,
templateUrl: 'button.html',
imports: [NgClass]
})
export class Button {
// Daisy UI Button Props as Angular Inputs
// Sizes: lg, md, sm, xs
size = input<'lg' | 'md' | 'sm' | 'xs' | null>(null);
// States: primary, secondary, accent, info, success, warning, error, ghost, link
variant = input<'primary' | 'secondary' | 'accent' | 'info' | 'success' | 'warning' | 'error' | 'ghost' | 'link' | null>(null);
// Outlined
outline = input<boolean>(false);
// Active
active = input<boolean>(false);
// Disabled
disabled = input<boolean>(false);
// Glass
glass = input<boolean>(false);
// Square
square = input<boolean>(false);
// Circle
circle = input<boolean>(false);
// Wide
wide = input<boolean>(false);
// Block
block = input<boolean>(false);
// No animation
noAnimation = input<boolean>(false);
// Loading
loading = input<boolean>(false);
// Responsive
responsive = input<boolean>(false);
// You can also add a custom class input if needed
customClass = input<string | null>(null);
@HostBinding('attr.disabled') get isDisabled() {
return this.disabled() ? true : null;
}
// New Output for the click event
// By default, it emits 'void' (no specific data), but you could specify a type if needed.
readonly clickEvent = output<MouseEvent>();
// Event handler method
onClick(event: MouseEvent): void {
// Prevent default browser behavior for buttons if needed (e.g., form submission)
// event.preventDefault();
// Emit the click event
this.clickEvent.emit(event);
}
}

View File

@ -1,26 +0,0 @@
<div class="card bg-base-100 w-96 shadow-sm" [class]="cardClass()">
@if (imageSrc()) {
<figure class="px-10 pt-10">
<img
[src]="imageSrc()"
[alt]="imageAlt()"
class="rounded-xl"
[class]="imageClass()"
/>
</figure>
}
<div class="card-body items-center text-center">
@if (cardTitle()) {
<h2 class="card-title">{{ cardTitle() }}</h2>
}
@if (cardText()) {
<p>A card component has a figure, a body part, and inside body there are title and actions parts</p>
}
<ng-content></ng-content>
@if (cardActionText()) {
<div class="card-actions">
<button class="btn btn-primary">{{ cardActionText() }}</button>
</div>
}
</div>
</div>

View File

@ -1,19 +0,0 @@
import { Component, input, output, signal } from '@angular/core';
@Component({
selector: 'rs-daisy-card-with-centered-content-and-paddings',
imports: [],
templateUrl: './card-with-centered-content-and-paddings.html',
styleUrl: './card-with-centered-content-and-paddings.css',
})
export class CardWithCenteredContentAndPaddings {
imageSrc = input<string | null>(null);
imageAlt = input<string | null>(null);
imageClass = input<string | null>(null);
cardTitle = input<string | null>(null);
cardText = input<string | null>(null);
cardActionText = input<string | null>(null);
cardActionClick = output<void>();
cardClass = signal<string | null>(null);
}

View File

@ -1,32 +0,0 @@
<footer class="footer sm:footer-horizontal bg-neutral text-neutral-content p-10">
@if (config()) {
@for ( footerNav of config()?.navs; let i = $index; track i){
<nav>
<h6 class="footer-title">{{ footerNav.label }}</h6>
@for ( link of footerNav.links; let j = $index; track j){
<a class="link link-hover" [routerLink]="link.href" >{{link.text}}</a>
}
</nav>
}
}
<nav>
<h6 class="footer-title">Services</h6>
<a class="link link-hover">Branding</a>
<a class="link link-hover">Design</a>
<a class="link link-hover">Marketing</a>
<a class="link link-hover">Advertisement</a>
</nav>
<nav>
<h6 class="footer-title">Company</h6>
<a class="link link-hover">About us</a>
<a class="link link-hover">Contact</a>
<a class="link link-hover">Jobs</a>
<a class="link link-hover">Press kit</a>
</nav>
<nav>
<h6 class="footer-title">Legal</h6>
<a class="link link-hover">Terms of use</a>
<a class="link link-hover">Privacy policy</a>
<a class="link link-hover">Cookie policy</a>
</nav>
</footer>

View File

@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Footer } from './footer';
describe('Footer', () => {
let component: Footer;
let fixture: ComponentFixture<Footer>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Footer]
})
.compileComponents();
fixture = TestBed.createComponent(Footer);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,17 +0,0 @@
import { Component, input } from '@angular/core';
import { FooterConfig } from '../../daisy.types';
import { RouterLink } from '@angular/router';
@Component({
selector: 'rs-daisy-footer',
imports: [
RouterLink,
],
templateUrl: './footer.html',
styleUrl: './footer.css',
})
export class Footer {
config = input<FooterConfig>()
}

View File

@ -1,15 +0,0 @@
{{ isOpen() }}
<dialog #modal class="modal" [class]="dialogStyleClass()">
<div class="modal-box" [class]="modalBoxStyleClass()">
<button (click)="closeClicked()" class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
@if (headerText()) {
<h3 class="text-lg font-bold">{{ headerText() }}</h3>
}
<ng-content></ng-content>
</div>
@if (backdrop()) {
<form (click)="closeClicked()" class="modal-backdrop">
<button>close</button>
</form>
}
</dialog>

View File

@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Modal } from './modal';
describe('Modal', () => {
let component: Modal;
let fixture: ComponentFixture<Modal>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Modal]
})
.compileComponents();
fixture = TestBed.createComponent(Modal);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,95 +0,0 @@
import {
AfterViewInit,
Component,
effect,
ElementRef,
input,
model, OnDestroy,
OnInit,
output,
signal,
ViewChild,
} from '@angular/core';
@Component({
selector: 'rs-daisy-modal',
imports: [],
templateUrl: './modal.html',
styleUrl: './modal.css',
})
export class Modal implements OnInit , AfterViewInit, OnDestroy{
dialogStyleClass = input<string>();
backdrop = input<boolean>(false);
modalBoxStyleClass = input<string>();
closeButton = input<boolean>(true);
headerText = input<string>();
isOpen = model<boolean>(false);
onClose = output<boolean>();
closeClick = output<boolean>();
@ViewChild('modal') modalRef!: ElementRef<HTMLDialogElement>;
private initialized = signal<boolean>(false);
constructor() {
effect(() => {
if ( this.isOpen()){
this.doOpen();
}else{
this.doClose();
}
});
}
ngOnInit() {
/* if (open()){
}*/
}
ngAfterViewInit() {
// console.info("dialog",this.dialog)
const modal = this.modalRef.nativeElement;
modal.addEventListener('close', () => this.onClose.emit(true) );
this.initialized.set(true);
}
emitClose(){
console.info("emit close", this.onClose);
this.onClose?.emit(true)
}
doOpen() {
if ( !this.initialized()){
return;
}
const modal = this.modalRef.nativeElement;
if ( !modal.open ){
modal.showModal();
}
}
doClose() {
if ( !this.initialized()){
return;
}
const modal = this.modalRef.nativeElement;
if ( modal.open ){
modal.close();
}
}
ngOnDestroy() {
console.info("destroy");
// const modal = this.modalRef?.nativeElement;
// if ( modal ){
// modal.removeEventListener('close', this.emitClose);
// }
}
closeClicked() {
console.info("close clicked");
this.closeClick.emit(true);
}
}

View File

@ -1,20 +0,0 @@
export interface Link{
href: string;
text: string;
}
export interface FooterNav{
label: string;
links: Link[];
}
export interface FooterConfig{
navs: FooterNav[]
}
export interface Breadcrumb{
text: string;
url?: string;
showIcon?: boolean;
svgIcon?: string;
}

View File

@ -1,63 +0,0 @@
<div class="drawer lg:drawer-open">
<input id="my-drawer-2" type="checkbox" class="drawer-toggle" />
<!-- Page Content -->
<div class="drawer-content flex flex-col h-screen">
<div class="navbar bg-base-200 p-4 shadow-md z-10">
<div class="flex-1">
<label for="my-drawer-2" class="btn btn-square btn-ghost drawer-button lg:hidden">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
class="inline-block w-6 h-6 stroke-current">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</label>
<a class="btn btn-ghost text-xl">{{headerText()}}</a>
</div>
@if (loggedIn()) {
<div class="flex-none">
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar">
<div class="w-10 rounded-full">
<img
alt="Tailwind CSS Navbar component"
src="https://img.daisyui.com/images/stock/photo-1534528741775-53994a69daeb.webp" />
</div>
</div>
<ul
tabindex="-1"
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow">
<li>
<a class="justify-between">
Profile
<span class="badge">New</span>
</a>
</li>
<li><a>Settings</a></li>
<li><a (click)="onClick($event,'logout')">Logout</a></li>
</ul>
</div>
</div>
}
</div>
<!-- Main Content -->
<main class="flex-1 p-6 bg-base-100 overflow-y-auto">
<ng-content select="[main-content]"></ng-content>
</main>
<!-- Footer -->
<footer class="bg-base-200 p-4 text-center">
<p>Copyright © 2025 - All right reserved</p>
</footer>
</div>
<!-- Sidebar -->
<div class="drawer-side">
<label for="my-drawer-2" aria-label="close sidebar" class="drawer-overlay"></label>
<ng-content select="[menu-content]"></ng-content>
</div>
</div>

View File

@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AdminLayoutRs1 } from './admin-layout-rs1';
describe('AdminLayoutRs1', () => {
let component: AdminLayoutRs1;
let fixture: ComponentFixture<AdminLayoutRs1>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AdminLayoutRs1]
})
.compileComponents();
fixture = TestBed.createComponent(AdminLayoutRs1);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,29 +0,0 @@
import { Component, input, output } from '@angular/core';
@Component({
selector: 'rs-daisy-admin-layout-rs1',
imports: [],
templateUrl: './admin-layout-rs1.html',
styleUrl: './admin-layout-rs1.css',
})
export class AdminLayoutRs1 {
headerText = input<string>();
readonly loggedIn = input<boolean>(false)
// New Output for the click event
// By default, it emits 'void' (no specific data), but you could specify a type if needed.
readonly clickEvent = output<MouseEvent>();
// Event handler method
onClick(event: MouseEvent, command: string): void {
// Prevent default browser behavior for buttons if needed (e.g., form submission)
// event.preventDefault();
// Emit the click event
this.clickEvent.emit(event);
}
}

View File

@ -1,3 +0,0 @@
export * from './admin-layout-rs1/admin-layout-rs1';

View File

@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NgDaisyui } from './ng-daisyui';
describe('NgDaisyui', () => {
let component: NgDaisyui;
let fixture: ComponentFixture<NgDaisyui>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NgDaisyui]
})
.compileComponents();
fixture = TestBed.createComponent(NgDaisyui);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,15 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'rs-daisy-ng-daisyui',
imports: [],
template: `
<p>
ng-daisyui works!
</p>
`,
styles: ``,
})
export class NgDaisyui {
}

View File

@ -1,16 +0,0 @@
import { inject, Pipe, PipeTransform } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
@Pipe({
name: 'safeHtml'
})
export class SafeHtmlPipe implements PipeTransform {
private sanitized = inject(DomSanitizer);
transform(value: string | undefined): SafeHtml {
if (!value) return '';
return this.sanitized.bypassSecurityTrustHtml(value);
}
}

View File

@ -1,12 +0,0 @@
/*
* Public API Surface of ng-daisyui
*/
export * from './lib/ng-daisyui';
export * from './lib/components/button/button';
export * from './lib/components/footer/footer';
export * from './lib/components/breadcrumbs/breadcrumbs';
export * from './lib/components/modal/modal';
export * from './lib/components/card/card-with-centered-content-and-paddings/card-with-centered-content-and-paddings';
export * from './lib/daisy.types';
export * from './lib/layout/';

View File

@ -1,18 +0,0 @@
/* 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. */
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"outDir": "../../../out-tsc/lib",
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": []
},
"include": [
"src/**/*.ts"
],
"exclude": [
"**/*.spec.ts"
]
}

View File

@ -1,11 +0,0 @@
/* 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. */
{
"extends": "./tsconfig.lib.json",
"compilerOptions": {
"declarationMap": false
},
"angularCompilerOptions": {
"compilationMode": "partial"
}
}

View File

@ -1,14 +0,0 @@
/* 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. */
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"outDir": "../../../out-tsc/spec",
"types": [
"jasmine"
]
},
"include": [
"src/**/*.ts"
]
}

View File

@ -1,8 +0,0 @@
{
"/api": {
"target": "http://localhost:3000",
"secure": false,
"logLevel": "debug",
"changeOrigin": true
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

View File

@ -1,23 +0,0 @@
import {
ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection,
provideZonelessChangeDetection,
} from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { routes } from './app.routes';
import { JwtInterceptor } from './auth/jwt.interceptor';
import { AuthService } from './auth/auth.service';
import { AuthGuard } from './auth/auth.guard';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZonelessChangeDetection(),
provideRouter(routes),
provideHttpClient(withInterceptorsFromDi()),
AuthService,
AuthGuard,
{ provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true },
],
};

View File

View File

@ -1,10 +0,0 @@
<rs-daisy-admin-layout-rs1 [headerText]="'Foglalási rendszer'" (clickEvent)="logout()" [loggedIn]="loggedIn()">
<app-menu
[title]="menuTitle"
[menuItems]="menuConfig"
[userRoles]="currentUserRoles"
menu-content>
</app-menu>
<router-outlet main-content />
</rs-daisy-admin-layout-rs1>

View File

@ -1,342 +0,0 @@
import { Routes } from '@angular/router';
import { LoginComponent } from './components/login/login.component';
import { AuthGuard } from './auth/auth.guard';
import { HomeComponent } from './components/home/home.component';
import { Welcome } from './page/welcome/welcome';
import { EventTypeFormComponent } from './features/event-type/components/event-type-form/event-type-form.component';
import {
EventTypeDetailsComponent,
} from './features/event-type/components/event-type-details/event-type-details.component';
import { EventTypeTableComponent } from './features/event-type/components/event-type-table/event-type-table.component';
import { EventTypeListComponent } from './features/event-type/components/event-type-list/event-type-list.component';
import { ProductFormComponent } from './features/products/components/product-form/product-form.component';
import { ProductDetailsComponent } from './features/products/components/product-details/product-details.component';
import { ProductTableComponent } from './features/products/components/product-table/product-table.component';
import { ProductListComponent } from './features/products/components/product-list/product-list.component';
import { EventFormComponent } from './features/events/components/event-form/event-form.component';
import { EventDetailsComponent } from './features/events/components/event-details/event-details.component';
import { EventTableComponent } from './features/events/components/event-table/event-table.component';
import { EventListComponent } from './features/events/components/event-list/event-list.component';
import { UserFormComponent } from './features/user/components/user-form/user-form.component';
import { UserDetailsComponent } from './features/user/components/user-details/user-details.component';
import { UserTableComponent } from './features/user/components/user-table/user-table.component';
import { UserListComponent } from './features/user/components/user-list/user-list.component';
import { UserGroupFormComponent } from './features/user-group/components/user-group-form/user-group-form.component';
import {
UserGroupDetailsComponent,
} from './features/user-group/components/user-group-details/user-group-details.component';
import { UserGroupTableComponent } from './features/user-group/components/user-group-table/user-group-table.component';
import { UserGroupListComponent } from './features/user-group/components/user-group-list/user-group-list.component';
import { UserRoleFormComponent } from './features/user-role/components/user-role-form/user-role-form.component';
import {
UserRoleDetailsComponent,
} from './features/user-role/components/user-role-details/user-role-details.component';
import { UserRoleTableComponent } from './features/user-role/components/user-role-table/user-role-table.component';
import { UserRoleListComponent } from './features/user-role/components/user-role-list/user-role-list.component';
import { BookingFormComponent } from "./features/bookings/components/booking-form/booking-form.component";
import { BookingDetailsComponent } from "./features/bookings/components/booking-details/booking-details.component";
import { BookingTableComponent } from "./features/bookings/components/booking-table/booking-table.component";
import { BookingListComponent } from "./features/bookings/components/booking-list/booking-list.component";
import { CalendarView } from './features/calendar/components/calendar-view/calendar-view';
export const routes: Routes = [
{
path: 'calendar',
component: CalendarView,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'bookings/new',
component: BookingFormComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'bookings',
component: BookingListComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'bookings/table',
component: BookingTableComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'bookings/:id',
component: BookingDetailsComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'bookings/:id/edit',
component: BookingFormComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'user-role/new',
component: UserRoleFormComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'user-role',
component: UserRoleListComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'user-role/table',
component: UserRoleTableComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'user-role/:id',
component: UserRoleDetailsComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'user-role/:id/edit',
component: UserRoleFormComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'user-group/new',
component: UserGroupFormComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'user-group',
component: UserGroupListComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'user-group/table',
component: UserGroupTableComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'user-group/:id',
component: UserGroupDetailsComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'user-group/:id/edit',
component: UserGroupFormComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'user/new',
component: UserFormComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'user',
component: UserListComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'user/table',
component: UserTableComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'user/:id',
component: UserDetailsComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'user/:id/edit',
component: UserFormComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'events/new',
component: EventFormComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'events',
component: EventListComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'events/table',
component: EventTableComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'events/:id',
component: EventDetailsComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'events/:id/edit',
component: EventFormComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'products',
component: ProductListComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'products/table',
component: ProductTableComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'products/:id',
component: ProductDetailsComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'products/:id/edit',
component: ProductFormComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'products/new',
component: ProductFormComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'event-type',
component: EventTypeListComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'event-type/table',
component: EventTypeTableComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'event-type/new',
component: EventTypeFormComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'event-type/:id',
component: EventTypeDetailsComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'event-type/:id/edit',
component: EventTypeFormComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{ path: 'login', component: LoginComponent },
{ path: 'welcome', component: Welcome },
{ path: 'home', component: HomeComponent, canActivate: [AuthGuard] },
{ path: '', component: Welcome },
{ path: '**', redirectTo: '' }, // Redirect to home for any other route
];

View File

@ -1,25 +0,0 @@
import { provideZonelessChangeDetection } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { App } from './app';
describe('App', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [App],
providers: [provideZonelessChangeDetection()]
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(App);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it('should render title', () => {
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, admin');
});
});

View File

@ -1,94 +0,0 @@
import { Component, inject, signal } from '@angular/core';
import { Router, RouterOutlet } from '@angular/router';
import { AuthService } from './auth/auth.service';
import { AdminLayoutRs1 } from '../../projects/rschneider/ng-daisyui/src/lib/layout';
import { Menu, MenuItem } from './components/menu/menu';
import { SingleEventDashboardCard } from './features/calendar/components/calendar-view/single-event-dashboard-card/single-event-dashboard-card';
@Component({
selector: 'app-root',
imports: [RouterOutlet, AdminLayoutRs1, Menu, SingleEventDashboardCard], // Import it here
templateUrl: './app.html',
styleUrl: './app.css',
})
export class App {
protected readonly title = signal('admin');
protected menuConfig: MenuItem[] = [];
protected currentUserRoles: string[] = ['admin'];
protected menuTitle: string | undefined = 'Menü';
constructor(private authService: AuthService, private router: Router) {
this.menuConfig = [
{
menuText: 'Esemény típusok',
targetUrl: '/event-type/table',
svgIcon: `<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="M6.429 9.75 2.25 12l4.179 2.25m0-4.5 5.571 3 5.571-3m-11.142 0L2.25 7.5 12 2.25l9.75 5.25-4.179 2.25m0 0L21.75 12l-4.179 2.25m0 0 4.179 2.25L12 21.75 2.25 16.5l4.179-2.25m11.142 0-5.571 3-5.571-3" />
</svg>
`,
},
{
menuText: 'Események',
targetUrl: '/events/table',
svgIcon: `<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="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5" />
</svg>
`,
},
{
menuText: 'Naptár',
targetUrl: '/calendar',
svgIcon: `<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="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5m-9-6h.008v.008H12v-.008ZM12 15h.008v.008H12V15Zm0 2.25h.008v.008H12v-.008ZM9.75 15h.008v.008H9.75V15Zm0 2.25h.008v.008H9.75v-.008ZM7.5 15h.008v.008H7.5V15Zm0 2.25h.008v.008H7.5v-.008Zm6.75-4.5h.008v.008h-.008v-.008Zm0 2.25h.008v.008h-.008V15Zm0 2.25h.008v.008h-.008v-.008Zm2.25-4.5h.008v.008H16.5v-.008Zm0 2.25h.008v.008H16.5V15Z" />
</svg>
`,
},
{
menuText: 'Felhasználók',
targetUrl: '/user/table',
svgIcon: `<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="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" />
</svg>
`,
},
{
menuText: 'Felhasználó Csoport',
targetUrl: '/user-group/table',
svgIcon: `<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="M18 18.72a9.094 9.094 0 0 0 3.741-.479 3 3 0 0 0-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 0 1 6 18.719m12 0a5.971 5.971 0 0 0-.941-3.197m0 0A5.995 5.995 0 0 0 12 12.75a5.995 5.995 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72 8.986 8.986 0 0 0 3.74.477m.94-3.197a5.971 5.971 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm6 3a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Zm-13.5 0a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Z" />
</svg>
`,
},
{
menuText: 'Felhasználó Szerepek',
targetUrl: '/user-role/table',
svgIcon: `<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="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75" />
</svg>
`,
},
];
}
logout(): void {
this.authService.serverSideLogout().subscribe({
next: () => {
console.log('Server-side logout successful.');
this.authService.clientSideLogout();
},
error: (err) => {
console.error('Server-side logout failed, logging out client-side anyway.', err);
this.authService.clientSideLogout();
},
});
}
loggedIn() {
return this.authService.isLoggedIn();
}
}

View File

@ -1,44 +0,0 @@
import { Injectable } from '@angular/core';
import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from './auth.service';
@Injectable({
providedIn: 'root',
})
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
const requiredRoles = route.data['roles'] as string[];
console.info("auth guard started", requiredRoles)
// 1. Check if the route requires any specific roles
if (!requiredRoles || requiredRoles.length === 0) {
// If no roles are required, only check if the user is logged in
return this.authService.isLoggedIn() ? true : this.router.parseUrl('/login');
}
// 2. Check if the user is logged in
if (!this.authService.isLoggedIn()) {
// If not logged in, redirect to the login page
return this.router.parseUrl('/login');
}
// 3. Check if the user has the required role
if (this.authService.hasRole(requiredRoles)) {
// If the user has the role, allow access
return true;
} else {
// If the user does not have the role, redirect to an unauthorized page
console.warn(`User does not have one of the required roles: ${requiredRoles}`);
return this.router.parseUrl('/welcome');
}
}
}

View File

@ -1,142 +0,0 @@
import { Injectable, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of, throwError } from 'rxjs';
import { tap } from 'rxjs/operators';
import { Router } from '@angular/router';
import { jwtDecode } from 'jwt-decode';
interface User {
name: string;
roles: string[];
}
export interface DecodedToken {
name: string; // Or 'username', 'given_name', etc.
roles: string[]; // The claim can be a single string or an array
exp: number; // Expiration time (Unix timestamp)
// Add other claims you need, like 'sub' for user ID
sub: string;
}
@Injectable({
providedIn: 'root',
})
export class AuthService {
private readonly ACCESS_TOKEN_KEY = 'accessToken';
private readonly REFRESH_TOKEN_KEY = 'refreshToken';
private apiUrl = 'http://localhost:4200/api/auth'; // Adjust if your server URL is different
currentUser = signal<User | null>(null);
constructor(private http: HttpClient, private router: Router) {
const accessToken = this.getAccessToken();
if (accessToken) {
this.decodeAndSetUser(accessToken);
}
}
login(credentials: { username: string; password: string }): Observable<any> {
return this.http.post<{ accessToken: string; refreshToken: string }>(`${this.apiUrl}/login`, credentials).pipe(
tap((response) => {
this.setTokens(response.accessToken, response.refreshToken);
this.decodeAndSetUser(response.accessToken);
})
);
}
/**
* Makes a best-effort call to the server to invalidate the refresh token.
*/
serverSideLogout(): Observable<any> {
return this.http.post(`${this.apiUrl}/logout`, {});
}
/**
* Performs the client-side cleanup, removing tokens and redirecting to login.
* This is the definitive logout action from the user's perspective.
*/
clientSideLogout(): void {
console.info("clientSideLogout");
this.removeTokens();
this.currentUser.set(null);
this.router.navigate(['/login']);
}
refreshToken(): Observable<any> {
console.info("getting refresh token");
const refreshToken = this.getRefreshToken();
if (!refreshToken) {
console.info("no refresh token found", refreshToken);
// If no refresh token is present, logout and return an error.
this.clientSideLogout();
return throwError(() => new Error('No refresh token available'));
}
console.info("refresh token found", refreshToken);
return this.http.post<{ accessToken: string; refreshToken: string }>(`${this.apiUrl}/refresh`, {}, {
headers: { Authorization: `Bearer ${refreshToken}` }
}).pipe(
tap((response) => {
this.setTokens(response.accessToken, response.refreshToken);
this.decodeAndSetUser(response.accessToken);
})
);
}
getAccessToken(): string | null {
return localStorage.getItem(this.ACCESS_TOKEN_KEY);
}
getRefreshToken(): string | null {
return localStorage.getItem(this.REFRESH_TOKEN_KEY);
}
isLoggedIn(): boolean {
return this.getAccessToken() !== null;
}
hasRole(requiredRoles: string[]): boolean {
const user = this.currentUser();
if (!user) {
return false; // Not logged in, so no roles
}
// Check if any of the user's roles match any of the required roles
return requiredRoles.some(requiredRole => user.roles.includes(requiredRole));
}
private setTokens(accessToken: string, refreshToken: string): void {
localStorage.setItem(this.ACCESS_TOKEN_KEY, accessToken);
localStorage.setItem(this.REFRESH_TOKEN_KEY, refreshToken);
}
private removeTokens(): void {
localStorage.removeItem(this.ACCESS_TOKEN_KEY);
localStorage.removeItem(this.REFRESH_TOKEN_KEY);
}
private decodeAndSetUser(token: string): void {
try {
const decodedToken: DecodedToken = jwtDecode(token);
// Check if the token is expired. exp is in seconds, Date.now() is in ms.
// if (decodedToken.exp * 1000 < Date.now()) {
// console.warn('Attempted to use an expired token.');
// this.logout(); // The token is expired, so log the user out
// return;
// }
this.currentUser.set({
name: decodedToken.name,
roles: decodedToken.roles
});
} catch (error) {
console.error('Failed to decode JWT:', error);
this.currentUser.set(null);
}
}
}

View File

@ -1,85 +0,0 @@
import { Injectable } from '@angular/core';
import {
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest,
HttpErrorResponse,
} from '@angular/common/http';
import { Observable, throwError, BehaviorSubject } from 'rxjs';
import { catchError, switchMap, filter, take, finalize } from 'rxjs/operators'; // Import finalize
import { AuthService } from './auth.service';
@Injectable()
export class JwtInterceptor implements HttpInterceptor {
private isRefreshing = false;
// Initialize refreshTokenSubject with null
private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
constructor(private authService: AuthService) {}
intercept(
request: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
if (request.url.includes('/auth/refresh')) {
return next.handle(request);
}
const accessToken = this.authService.getAccessToken();
if (accessToken) {
request = this.addToken(request, accessToken);
}
return next.handle(request).pipe(
catchError((error) => {
if (error instanceof HttpErrorResponse && error.status === 401) {
return this.handle401Error(request, next);
}
return throwError(() => error);
})
);
}
private handle401Error(request: HttpRequest<any>, next: HttpHandler): Observable<any> {
if (!this.isRefreshing) {
this.isRefreshing = true;
// Reset the refreshTokenSubject to null so that subsequent requests will wait
// this.refreshTokenSubject.next(null);
this.refreshTokenSubject = new BehaviorSubject<any>(null);
console.info("Refreshing tokens");
return this.authService.refreshToken().pipe(
switchMap((token: any) => {
this.refreshTokenSubject.next(token.accessToken);
return next.handle(this.addToken(request, token.accessToken));
}),
catchError((err) => {
// If refresh fails, logout the user
this.authService.clientSideLogout();
return throwError(() => err);
}),
finalize(() => {
console.info("refreshing done")
// When the refresh attempt completes, set isRefreshing to false
this.isRefreshing = false;
})
);
} else {
// If a refresh is already in progress, wait for it to complete
return this.refreshTokenSubject.pipe(
filter(token => token != null),
take(1),
switchMap(jwt => next.handle(this.addToken(request, jwt)))
);
}
}
private addToken(request: HttpRequest<any>, token: string) {
return request.clone({
setHeaders: {
Authorization: `Bearer ${token}`,
},
});
}
}

View File

@ -1,8 +0,0 @@
@if (color()) {
<div class="flex flex-row gap-1 items-center">
<div class='min-w-3 min-h-3 w-3 h-3'
[style]="'background-color: ' + color()"
></div>
{{color()}}
</div>
}

View File

@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ColorView } from './color-view';
describe('ColorView', () => {
let component: ColorView;
let fixture: ComponentFixture<ColorView>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ColorView]
})
.compileComponents();
fixture = TestBed.createComponent(ColorView);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,11 +0,0 @@
import { Component, input } from '@angular/core';
@Component({
selector: 'app-color-view',
imports: [],
templateUrl: './color-view.html',
styleUrl: './color-view.css',
})
export class ColorView {
color = input<string>();
}

View File

@ -1,27 +0,0 @@
@if (config().data){
<div class="overflow-x-auto" [ngClass]="config().styleClass">
<table class="table w-full table-zebra">
<tbody>
@for (row of config().rows; track row.attribute) {
<tr [ngClass]="row.getDetailClass ? row.getDetailClass(config().data) : ''">
<th [ngClass]="row.getTitleStyleClass ? row.getTitleStyleClass(config().data) : ''">
@if (row.titleComponent) {
<ng-container *ngComponentOutlet="row.titleComponent; inputs: row.titleComponentInputs ? row.titleComponentInputs(config().data) : {}"></ng-container>
} @else {
{{ getTitle(row, config().data) }}
}
</th>
<td [ngClass]="row.getValueStyleClass ? row.getValueStyleClass(config().data) : ''">
@if (row.component) {
<ng-container *ngComponentOutlet="row.component; inputs: row.componentInputs ? row.componentInputs(config().data) : {}"></ng-container>
} @else {
{{ getFormattedValue(row, config().data) }}
}
</td>
</tr>
}
</tbody>
</table>
</div>
}

View File

@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DetailView } from './detail-view';
describe('DetailView', () => {
let component: DetailView;
let fixture: ComponentFixture<DetailView>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DetailView]
})
.compileComponents();
fixture = TestBed.createComponent(DetailView);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,76 +0,0 @@
import { Component, input, Type } from '@angular/core';
import { CommonModule } from '@angular/common';
export interface CellComponent<T> {
component?: Type<any>;
componentInputs?: (obj: T) => { [key: string]: any };
}
export interface DetailViewRow<T> extends CellComponent<T> {
attribute: keyof T;
getValue?: (obj: T) => any;
format?: 'date' | 'datetime' | 'number' | 'raw' | ((value: any) => string);
getTitle?: string | ((obj: T) => string);
getDetailClass?: (obj: T) => string;
getTitleStyleClass?: (obj: T) => string;
getValueStyleClass?: (obj: T) => string;
titleComponent?: Type<any>;
titleComponentInputs?: (obj: T) => { [key: string]: any };
}
export interface DetailViewConfig<T> {
data: T;
rows: DetailViewRow<T>[];
styleClass?: string;
}
@Component({
selector: 'app-detail-view',
imports: [CommonModule],
templateUrl: './detail-view.html',
styleUrl: './detail-view.css',
})
export class DetailView<T> {
config = input.required<DetailViewConfig<T>>();
getFormattedValue(row: DetailViewRow<T>, data: T): string {
let value : any;
try {
value = row.getValue ? row.getValue(data) : data[row.attribute];
}catch (e) {
value = '';
}
if (row.component) {
return '';
}
if (typeof row.format === 'function') {
return row.format(value);
}
switch (row.format) {
case 'date':
return new Date(value).toLocaleDateString();
case 'datetime':
return new Date(value).toLocaleString();
case 'number':
return Number(value).toString();
case 'raw':
default:
return value;
}
}
getTitle(row: DetailViewRow<T>, data: T): string {
if (row.titleComponent) {
return '';
}
if (typeof row.getTitle === 'function') {
return row.getTitle(data);
}
return row.getTitle || String(row.attribute);
}
}

View File

@ -1,22 +0,0 @@
<footer class="footer sm:footer-horizontal bg-neutral text-neutral-content p-10">
<nav>
<h6 class="footer-title">Services</h6>
<a class="link link-hover">Branding</a>
<a class="link link-hover">Design</a>
<a class="link link-hover">Marketing</a>
<a class="link link-hover">Advertisement</a>
</nav>
<nav>
<h6 class="footer-title">Company</h6>
<a class="link link-hover">About us</a>
<a class="link link-hover">Contact</a>
<a class="link link-hover">Jobs</a>
<a class="link link-hover">Press kit</a>
</nav>
<nav>
<h6 class="footer-title">Legal</h6>
<a class="link link-hover">Terms of use</a>
<a class="link link-hover">Privacy policy</a>
<a class="link link-hover">Cookie policy</a>
</nav>
</footer>

View File

@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Footer } from './footer';
describe('Footer', () => {
let component: Footer;
let fixture: ComponentFixture<Footer>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Footer]
})
.compileComponents();
fixture = TestBed.createComponent(Footer);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,11 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-footer',
imports: [],
templateUrl: './footer.html',
styleUrl: './footer.css',
})
export class Footer {
}

View File

@ -1,13 +0,0 @@
<div class="flex flex-row gap-1 items-center">
@for (action of actions(); track action){
<a class="btn btn-primary" title="{{action.title ?? '' }}" (click)="onClick($event,action)">
@if(action.svgIcon){
<span [outerHTML]="action.svgIcon | safeHtml"></span>
}
@if ( action.text !== false){
{{action.text || action.action}}
}
</a>
}
</div>

View File

@ -1,78 +0,0 @@
import { Component, inject, input, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { SafeHtmlPipe } from '../../pipes/safe-html-pipe';
import { SvgIcons } from '../../svg-icons';
import { EventType } from '../../features/event-type/models/event-type.model';
import { CellDefinition, StyleClassContext } from '../generic-table/cell-definition.interface';
export interface ActionDefinition<T> {
action: string;
text?: string | false;
title?: string;
generate: (action: string, item?: T) => string;
handler?: (action: ActionDefinition<T>, item?: T) => void;
svgIcon?: string; //https://heroicons.com/
}
@Component({
selector: 'app-generic-action-column',
imports: [
SafeHtmlPipe,
],
templateUrl: './generic-action-column.html',
styleUrl: './generic-action-column.css',
standalone: true,
})
export class GenericActionColumn<T> implements OnInit {
private router = inject(Router);
actions = input([] as ActionDefinition<T>[]);
item = input(undefined as T);
payload = input(undefined as any);
ngOnInit(): void {
}
onClick($event: any, actionDefinition: ActionDefinition<T>) {
if (actionDefinition?.handler) {
actionDefinition.handler(actionDefinition, this.item());
}
}
}
export interface CreateActionColumnCellDefinitionContext<T> {
actionHandler?: (action: ActionDefinition<T>, item?: T) => void;
styleClass?: (definition: StyleClassContext<T>) => string;
}
export const createActionColumnCellDefinition = <T>(createCtx: CreateActionColumnCellDefinitionContext<T>): CellDefinition<T> => {
return {
styleClass: createCtx.styleClass ?? (ctx => 'w-[1%]'),
component: GenericActionColumn,
componentInputs: item => ({
item: item,
actions: [
{
action: 'view',
title: 'Részletek',
text: false,
handler: createCtx.actionHandler,
svgIcon: SvgIcons.heroDocument,
},
{
action: 'edit', title: 'Szerkesztés', text: false,
svgIcon: SvgIcons.heroCog6Tooth,
handler: createCtx.actionHandler,
},
{
action: 'delete',
title: 'Törlés',
text: false,
handler: createCtx.actionHandler,
svgIcon: SvgIcons.heroTrash,
},
] as ActionDefinition<EventType>[],
}),
};
};

View File

@ -1,15 +0,0 @@
import { Type } from '@angular/core';
import { ColumnDefinition } from './column-definition.interface';
export interface StyleClassContext<T>{
definition: ColumnDefinition<T> ;
cellDefinition: CellDefinition<T>;
item?: T;
}
export interface CellDefinition<T> {
value?: ((item?: T) => any) | string;
component?: Type<any>;
componentInputs?: (item?: T|null) => { [key: string]: any };
styleClass?: (definition: StyleClassContext<T>) => string;
}

View File

@ -1,13 +0,0 @@
import { CellDefinition } from './cell-definition.interface';
export interface TypeDefinition{
type: 'boolean' | 'number' | 'string' | 'date' | 'time' | 'datetime';
params?: Record<string, any>;
}
export interface ColumnDefinition<T> {
attribute: keyof T;
type: TypeDefinition;
valueCell?: CellDefinition<T> | boolean
headerCell?: CellDefinition<T> | boolean
}

View File

@ -1,23 +0,0 @@
import { Observable } from 'rxjs';
import { PaginatedResponse } from '../../features/products/models/product.model';
export interface QueryParams extends Record<string, any>{
sortBy?: string;
sortDirection?: 'asc' | 'desc',
page?: number,
limit?: number,
q?: string,
}
export interface GetDataOptions extends Record<string, any>{
params?: QueryParams
}
export interface GetDataResponse<T> extends Record<string, any>{
data: PaginatedResponse<T>;
}
export interface DataProvider<T> {
getData( options?: GetDataOptions): Observable<GetDataResponse<T>>;
}

View File

@ -1,6 +0,0 @@
<form [formGroup]="filterForm" class="p-4 bg-base-200 rounded-lg shadow">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 items-end">
<div class="form-control"><label class="label"><span class="label-text">SearchTerm</span></label>
<input type="text" formControlName="term" class="input input-bordered w-full" placeholder="Search term" /></div>
</div>
</form>

View File

@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { GenericTableSearchForm } from './generic-table-search-form';
describe('GenericTableSearchForm', () => {
let component: GenericTableSearchForm;
let fixture: ComponentFixture<GenericTableSearchForm>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [GenericTableSearchForm]
})
.compileComponents();
fixture = TestBed.createComponent(GenericTableSearchForm);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,37 +0,0 @@
import { Component, EventEmitter, Output } from '@angular/core';
import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { debounceTime, distinctUntilChanged, filter } from 'rxjs/operators';
@Component({
selector: 'app-generic-table-search-form',
imports: [
FormsModule,
ReactiveFormsModule,
],
templateUrl: './generic-table-search-form.html',
styleUrl: './generic-table-search-form.css',
})
export class GenericTableSearchForm {
@Output() searchTermChanged = new EventEmitter<any>();
filterForm: FormGroup;
constructor(private fb: FormBuilder) {
this.filterForm = this.fb.group({
term: ['']
});
this.filterForm.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged()
).subscribe(values => {
const cleanFilter = Object.fromEntries(
Object.entries(values).filter(([_, v]) => v != null && v !== '')
);
this.searchTermChanged.emit(cleanFilter);
});
}
reset() {
this.filterForm.reset();
}
}

View File

@ -1,14 +0,0 @@
import { DataProvider } from './data-provider.interface';
import { ColumnDefinition } from './column-definition.interface';
import { Subject } from 'rxjs';
export interface GenericTableConfig<T> {
dataProvider: DataProvider<T>;
columns: ColumnDefinition<T>[];
tableCssClass?: string;
rowCssClass?: (item: T) => string;
refresh$: Subject<void>;
filter$: Subject<any>;
page$: Subject<number>;
limit$: Subject<number>;
}

View File

@ -1,92 +0,0 @@
<div [ngClass]="config.tableCssClass" [class]="'overflow-x-auto'">
<app-generic-table-search-form (searchTermChanged)="searchTermChanged($event)" ></app-generic-table-search-form>
<div>
</div>
@if (data$ | async; as getDataResponse) {
<table class="table w-full table-zebra" [class]="config.tableCssClass ?? ''">
<thead>
<tr>
@for (column of config.columns; track column) {
<th [class]="column.attribute">
@if (column.headerCell) {
@if (typeof column.headerCell === 'boolean') {
{{ column.attribute }}
} @else {
@if (!column?.headerCell?.component) {
@if (typeof column.headerCell.value === 'string') {
<div [innerHTML]="getAsHtml(column.headerCell.value)"></div>
}
} @else {
<ng-container
*ngComponentOutlet="column.headerCell.component!"></ng-container>
}
}
}
</th>
}
</tr>
</thead>
<tbody>
@for (item of getDataResponse?.data?.data; track item) {
<tr [ngClass]="config.rowCssClass ? config.rowCssClass(item) : ''">
@for (column of config.columns; track column) {
<td [class]="getValueStyleClass(column!,column.valueCell!,item) ">
@if (column.valueCell) {
@if (typeof column.valueCell === 'boolean') {
<div [outerHTML]=" resolveValue(item, column) | safeHtml "></div>
} @else {
@if (!column.valueCell.component) {
<div [outerHTML]=" resolveValue(item, column, column.valueCell) | safeHtml "></div>
} @else {
<ng-container
*ngComponentOutlet="column.valueCell.component; inputs: getComponentInputs(item, column,column.valueCell)"></ng-container>
}
}
}
</td>
}
</tr>
}
</tbody>
</table>
@if ((getDataResponse?.data?.meta?.totalPages ?? 1) > 0) {
<div class="flex justify-end mt-4 items-center me-3 ">
Items: {{getDataResponse.data.data.length}}/{{getDataResponse.data.meta.totalItems}}
Page: {{getDataResponse.data.meta.currentPage}}/{{getDataResponse.data.meta.totalPages}}
PageSize:
<select class="select w-auto" #pageSize (change)="onPageSizeChange(pageSize.value)">
<option>10</option>
<option>20</option>
<option>50</option>
</select>
</div>
}
@if ((getDataResponse?.data?.meta?.totalPages ?? 1) > 1) {
<div class="flex justify-center mt-4">
<div class="join">
<button
class="join-item btn"
(click)="changePage(getDataResponse.data.meta.currentPage- 1)"
[disabled]="getDataResponse.data.meta.currentPage === 1">«
</button>
<button class="join-item btn">Page {{ getDataResponse.data.meta.currentPage }} of {{ getDataResponse.data.meta.totalPages }}</button>
<button
class="join-item btn"
(click)="changePage(getDataResponse.data.meta.currentPage + 1)"
[disabled]="getDataResponse.data.meta.currentPage === getDataResponse.data.meta.totalPages">»
</button>
</div>
</div>
}
} @else {
<div class="text-center p-8">
<span class="loading loading-spinner loading-lg"></span>
</div>
}
</div>

View File

@ -1,108 +0,0 @@
import { Component, inject, Input, OnInit } from '@angular/core';
import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs';
import { ColumnDefinition } from './column-definition.interface';
import { AsyncPipe, NgClass, NgComponentOutlet } from '@angular/common';
import { CellDefinition } from './cell-definition.interface';
import { GetDataResponse } from './data-provider.interface';
import { DomSanitizer } from '@angular/platform-browser';
import { GenericTableConfig } from './generic-table.config';
import { startWith, switchMap } from 'rxjs/operators';
import { GenericTableSearchForm } from './generic-table-search-form/generic-table-search-form';
import { SafeHtmlPipe } from '../../pipes/safe-html-pipe';
@Component({
selector: 'app-generic-table',
templateUrl: './generic-table.html',
imports: [NgClass, AsyncPipe, NgComponentOutlet, GenericTableSearchForm, SafeHtmlPipe],
standalone: true,
})
export class GenericTable<T> implements OnInit {
@Input() config!: GenericTableConfig<T>;
public data$!: Observable<GetDataResponse<T>>;
sanitizer = inject(DomSanitizer);
parser = new DOMParser();
ngOnInit(): void {
this.data$ = combineLatest([
this.config.refresh$,
this.config.filter$.pipe(startWith({})),
this.config.page$.pipe(startWith(1)),
this.config.limit$.pipe(startWith(10)),
]).pipe(
switchMap(([_, filter, page, limit]) => {
const query = { ...filter, page, limit: 10 };
console.info('filter is', filter);
return this.config.dataProvider.getData({
params: {
q: filter.term ?? '',
page,
limit,
},
});
}),
);
}
resolveValue(item: T, column: ColumnDefinition<T>, cell?: CellDefinition<T>): any {
if (cell) {
if (cell.value) {
if (typeof cell.value === 'string') {
return cell.value;
}
if (typeof cell.value === 'function') {
return cell.value(item);
}
}
}
if (column.attribute) {
return item[column.attribute];
}
return '';
}
getComponentInputs(item: T | null, column: ColumnDefinition<T>, cell: CellDefinition<T>): { [key: string]: any } {
if (cell.componentInputs) {
return cell.componentInputs(item);
}
return { item }; // Default input
}
getAsHtml(str: string) {
// return this.sanitizer.bypassSecurityTrustHtml(str);
return this.sanitizer.bypassSecurityTrustHtml(this.parser.parseFromString(str, 'text/html').body.innerHTML);
}
changePage(newPage: number): void {
if (newPage > 0) {
this.config.page$.next(newPage);
}
}
protected searchTermChanged(searchTerm: any) {
console.info('searchterm', searchTerm);
this.config.page$.next(1);
this.config.filter$.next(searchTerm);
}
protected onPageSizeChange(value: any) {
this.config.page$.next(1);
this.config.limit$.next(+value);
}
protected getValueStyleClass(columnDefinition: ColumnDefinition<T>, cellDefinition: CellDefinition<T> | boolean, item: T) {
console.info("co")
if ( (cellDefinition as any).hasOwnProperty("styleClass") ){
const def = (cellDefinition as CellDefinition<T>);
if (def && def.styleClass) {
return (columnDefinition.attribute as string) +" "+def.styleClass({definition: columnDefinition, cellDefinition: def, item});
}
}
return columnDefinition.attribute;
}
}

View File

@ -1,8 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-home',
template: '<h1>Welcome to the Admin Panel!</h1>',
standalone: true,
})
export class HomeComponent {}

View File

@ -1,17 +0,0 @@
<div class="bg-base-200 rounded-2xl p-4">
<h2 class="">Login</h2>
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<div class="pt-4">
<label class="label pe-2" for="username">Username:</label>
<input id="username" class="input " formControlName="username" />
</div>
<div class="pt-4">
<label class="label pe-2" for="password">Password:</label>
<input id="password" class="input " type="password" formControlName="password" />
</div>
<div class="button-container pt-4">
<rs-daisy-button type="submit" [disabled]="loginForm.invalid">Log In</rs-daisy-button>
</div>
</form>
<p *ngIf="errorMessage">{{ errorMessage }}</p>
</div>

View File

@ -1,38 +0,0 @@
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { AuthService } from '../../auth/auth.service';
import { CommonModule } from '@angular/common';
import {Button} from '@rschneider/ng-daisyui';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
standalone: true,
imports: [ReactiveFormsModule, CommonModule, Button],
})
export class LoginComponent {
loginForm: FormGroup;
errorMessage: string = '';
constructor(
private fb: FormBuilder,
private authService: AuthService,
private router: Router
) {
this.loginForm = this.fb.group({
username: ['', Validators.required],
password: ['', Validators.required],
});
}
onSubmit(): void {
if (this.loginForm.valid) {
this.authService.login(this.loginForm.value).subscribe({
next: () => this.router.navigate(['/']),
error: (err) => (this.errorMessage = 'Invalid username or password'),
});
}
}
}

View File

@ -1,52 +0,0 @@
<!--
<ul class="menu p-4 w-80 min-h-full bg-base-300 text-base-content">
<li class="text-lg font-bold p-4">My App</li>
<li><a>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
Dashboard</a></li>
<li><a>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Analytics</a></li>
<li><a>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
Reports</a></li>
</ul>
-->
<ul class="menu p-4 w-80 min-h-full bg-base-300 text-base-content">
@if(title()){
<li class="text-lg font-bold p-4">{{ title() }}</li>
}
@for (item of menuItems(); track item.menuText) {
@if (isItemVisible(item)) {
<li [ngClass]="getItemStyleClass(item)">
@if (item.children && item.children.length > 0) {
<details [open]="isSubMenuActive(item)">
<summary>
<!-- Using [innerHTML] for SVG. Be cautious with non-trusted sources. -->
<span [innerHTML]="item.svgIcon | safeHtml"></span>
{{ item.menuText }}
</summary>
<!-- Recursive call to the menu component for nested items -->
<app-menu [menuItems]="item.children" [userRoles]="userRoles()"></app-menu>
</details>
} @else {
<a (click)="handleItemClick($event, item)" [class.menu-active]="isItemActive(item)">
<span [innerHTML]="item.svgIcon | safeHtml"></span>
{{ item.menuText }}
</a>
}
</li>
}
}
</ul>

View File

@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Menu } from './menu';
describe('Menu', () => {
let component: Menu;
let fixture: ComponentFixture<Menu>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Menu]
})
.compileComponents();
fixture = TestBed.createComponent(Menu);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,82 +0,0 @@
import { Component, inject, input } from '@angular/core';
import { Router, RouterModule } from '@angular/router';
import { CommonModule } from '@angular/common';
import { SafeHtmlPipe } from '../../pipes/safe-html-pipe';
export interface MenuItem {
id?: string;
menuText: string;
targetUrl?: string;
onClick?: (event: MouseEvent, item: MenuItem) => void;
roles?: string[];
children?: MenuItem[];
getStyleClass?: (item: MenuItem) => string;
isActive?: (item: MenuItem, router: Router) => boolean;
svgIcon?: string;
}
@Component({
selector: 'app-menu',
imports: [CommonModule, RouterModule, SafeHtmlPipe],
templateUrl: './menu.html',
styleUrl: './menu.css',
})
export class Menu {
title = input<string>();
// Use the input() function instead of the @Input() decorator.
// input.required makes sure that the menuItems array is always passed.
menuItems = input.required<MenuItem[]>();
// You can also provide a default value with input().
// In a real app, this would likely come from an authentication service.
userRoles = input<string[]>(['admin', 'user']);
// Use inject() for dependency injection instead of the constructor.
private router = inject(Router);
isItemVisible(item: MenuItem): boolean {
if (!item.roles || item.roles.length === 0) {
return true;
}
// Access the value of the signal by calling it as a function: this.userRoles()
return item.roles.some(role => this.userRoles().includes(role));
}
handleItemClick(event: MouseEvent, item: MenuItem): void {
if (item.onClick) {
item.onClick(event, item);
} else if (item.targetUrl) {
this.router.navigate([item.targetUrl]);
}
}
getItemStyleClass(item: MenuItem): string {
return item.getStyleClass ? item.getStyleClass(item) : '';
}
isItemActive(item: MenuItem): boolean {
if (item.isActive) {
return item.isActive(item, this.router);
}
if (item.targetUrl) {
// router.isActive is a boolean check, perfect for this use case.
return this.router.isActive(item.targetUrl, {
paths: 'exact',
queryParams: 'exact',
fragment: 'ignored',
matrixParams: 'ignored'
});
}
return false;
}
// Helper function to determine if a dropdown should be open by default
isSubMenuActive(item: MenuItem): boolean {
if (!item.children) {
return false;
}
// Recursively check if any child or grandchild is active
return item.children.some(child => this.isItemActive(child) || this.isSubMenuActive(child));
}
}

View File

@ -1,19 +0,0 @@
<div class="navbar bg-base-100 shadow-sm">
<div class="flex-1">
<a class="btn btn-ghost text-xl">daisyUI</a>
</div>
<div class="flex-none">
<ul class="menu menu-horizontal px-1">
<li><a>Link</a></li>
<li>
<details>
<summary>Parent</summary>
<ul class="bg-base-100 rounded-t-none p-2">
<li><a>Link 1</a></li>
<li><a>Link 2</a></li>
</ul>
</details>
</li>
</ul>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More