Merge pull request '1.2' (#11) from main into prod
All checks were successful
Release / Release (push) Successful in 2m8s

Reviewed-on: #11
This commit is contained in:
Jan Gleytenhoover 2025-01-21 12:37:12 +00:00
commit 2583ed7097
40 changed files with 409 additions and 172 deletions

View file

@ -29,4 +29,3 @@ jobs:
with: with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}

3
.prettierignore Normal file
View file

@ -0,0 +1,3 @@
# Ignore artifacts:
build
coverage

3
.prettierrc Normal file
View file

@ -0,0 +1,3 @@
{
"bracketSpacing": true
}

View file

@ -16,9 +16,7 @@
"outputPath": "dist/jklink", "outputPath": "dist/jklink",
"index": "src/index.html", "index": "src/index.html",
"browser": "src/main.ts", "browser": "src/main.ts",
"polyfills": [ "polyfills": ["zone.js"],
"zone.js"
],
"tsConfig": "tsconfig.app.json", "tsConfig": "tsconfig.app.json",
"assets": [ "assets": [
{ {
@ -86,10 +84,7 @@
"test": { "test": {
"builder": "@angular-devkit/build-angular:karma", "builder": "@angular-devkit/build-angular:karma",
"options": { "options": {
"polyfills": [ "polyfills": ["zone.js", "zone.js/testing"],
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json", "tsConfig": "tsconfig.spec.json",
"assets": [ "assets": [
{ {

17
package-lock.json generated
View file

@ -38,6 +38,7 @@
"karma-jasmine": "~5.1.0", "karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0", "karma-jasmine-html-reporter": "~2.1.0",
"postcss": "^8.5.1", "postcss": "^8.5.1",
"prettier": "3.4.2",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"typescript": "~5.6.2" "typescript": "~5.6.2"
} }
@ -10845,6 +10846,22 @@
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
}, },
"node_modules/prettier": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
"integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/proc-log": { "node_modules/proc-log": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz",

View file

@ -40,6 +40,7 @@
"karma-jasmine": "~5.1.0", "karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0", "karma-jasmine-html-reporter": "~2.1.0",
"postcss": "^8.5.1", "postcss": "^8.5.1",
"prettier": "3.4.2",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"typescript": "~5.6.2" "typescript": "~5.6.2"
} }

View file

@ -1,11 +1,14 @@
module.exports = { module.exports = {
branches: ['prod'], branches: ["prod"],
plugins: [ plugins: [
'@semantic-release/commit-analyzer', "@semantic-release/commit-analyzer",
'@semantic-release/release-notes-generator', "@semantic-release/release-notes-generator",
'@semantic-release/changelog', "@semantic-release/changelog",
["@saithodev/semantic-release-gitea", { [
"giteaUrl": "https://git.kjan.de" "@saithodev/semantic-release-gitea",
}], {
giteaUrl: "https://git.kjan.de",
},
],
], ],
}; };

View file

@ -24,6 +24,8 @@ describe('AppComponent', () => {
const fixture = TestBed.createComponent(AppComponent); const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges(); fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement; const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, jklink'); expect(compiled.querySelector('h1')?.textContent).toContain(
'Hello, jklink',
);
}); });
}); });

View file

@ -5,7 +5,6 @@ import { RouterOutlet } from '@angular/router';
selector: 'app-root', selector: 'app-root',
imports: [RouterOutlet], imports: [RouterOutlet],
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.css' styleUrl: './app.component.css',
}) })
export class AppComponent { export class AppComponent {}
}

View file

@ -5,5 +5,9 @@ import { routes } from './app.routes';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideAnimationsAsync()] providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideAnimationsAsync(),
],
}; };

View file

@ -25,7 +25,7 @@ export const routes: Routes = [
component: ViewLinkComponent, component: ViewLinkComponent,
}, },
{ {
path: "**", path: '**',
redirectTo: "", redirectTo: '',
}, },
]; ];

View file

@ -0,0 +1,6 @@
<h2 mat-dialog-title>{{ data.title }}</h2>
<mat-dialog-content>{{ data.description }}</mat-dialog-content>
<mat-dialog-actions>
<button mat-flat-button color="warn" (click)="close()">No</button>
<button mat-flat-button (click)="accept()">Yes</button>
</mat-dialog-actions>

View file

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

View file

@ -0,0 +1,35 @@
import { Component, inject } from '@angular/core';
import {
MatDialog,
MAT_DIALOG_DATA,
MatDialogTitle,
MatDialogContent,
MatDialogActions,
MatDialogRef,
} from '@angular/material/dialog';
import { MatButtonModule } from '@angular/material/button';
import { ConfirmationModalDto } from '../models/confirmation-modal';
@Component({
selector: 'app-confirmation-modal',
imports: [
MatDialogTitle,
MatDialogContent,
MatDialogActions,
MatButtonModule,
],
templateUrl: './confirmation-modal.component.html',
styleUrl: './confirmation-modal.component.css',
})
export class ConfirmationModalComponent {
public data: ConfirmationModalDto = inject(MAT_DIALOG_DATA);
constructor(private dialogRef: MatDialogRef<ConfirmationModalComponent>) {}
close() {
this.dialogRef.close(false);
}
accept() {
this.dialogRef.close(true);
}
}

View file

@ -1,17 +1,24 @@
<div class="mx-auto container"> <div class="mx-auto container">
<app-navbar></app-navbar> <app-navbar></app-navbar>
<button mat-flat-button class="mt-3" color="warn" (click)="back()">Back</button> <button mat-flat-button class="mt-3" color="warn" (click)="back()">
Back
</button>
<mat-card class="mt-3 p-3" appearance="outlined"> <mat-card class="mt-3 p-3" appearance="outlined">
<form class="flex flex-col" [formGroup]="createLinkForm"> <form class="flex flex-col" [formGroup]="createLinkForm">
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-error>{{errorMessages['name']}}</mat-error> <mat-error>{{ errorMessages["name"] }}</mat-error>
<mat-label>Name</mat-label> <mat-label>Name</mat-label>
<input formControlName="name" matInput placeholder="My Awesome link"> <input formControlName="name" matInput placeholder="My Awesome link" />
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-error>{{errorMessages['link']}}</mat-error> <mat-error>{{ errorMessages["link"] }}</mat-error>
<mat-label>Link</mat-label> <mat-label>Link</mat-label>
<input formControlName="link" matInput type="url" placeholder="https://kjan.de"> <input
formControlName="link"
matInput
type="url"
placeholder="https://kjan.de"
/>
</mat-form-field> </mat-form-field>
<button mat-flat-button type="submit" (click)="submit()">Create</button> <button mat-flat-button type="submit" (click)="submit()">Create</button>
</form> </form>

View file

@ -8,9 +8,8 @@ describe('CreateLinkComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [CreateLinkComponent] imports: [CreateLinkComponent],
}) }).compileComponents();
.compileComponents();
fixture = TestBed.createComponent(CreateLinkComponent); fixture = TestBed.createComponent(CreateLinkComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View file

@ -1,6 +1,11 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { NavbarComponent } from '../navbar/navbar.component'; import { NavbarComponent } from '../navbar/navbar.component';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import {
FormControl,
FormGroup,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
import { LinkService } from '../service/link.service'; import { LinkService } from '../service/link.service';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
@ -11,31 +16,45 @@ import { debounceTime } from 'rxjs';
@Component({ @Component({
selector: 'app-create-link', selector: 'app-create-link',
imports: [NavbarComponent, ReactiveFormsModule, MatCardModule, MatFormFieldModule, MatInputModule, MatButtonModule], imports: [
NavbarComponent,
ReactiveFormsModule,
MatCardModule,
MatFormFieldModule,
MatInputModule,
MatButtonModule,
],
templateUrl: './create-link.component.html', templateUrl: './create-link.component.html',
styleUrl: './create-link.component.css' styleUrl: './create-link.component.css',
}) })
export class CreateLinkComponent { export class CreateLinkComponent {
public createLinkForm!: FormGroup; public createLinkForm!: FormGroup;
public requestFailed: boolean = false; public requestFailed: boolean = false;
public errorMessages: Record<string, string> = {}; public errorMessages: Record<string, string> = {};
constructor(private linkService: LinkService, private router: Router) { } constructor(
private linkService: LinkService,
private router: Router,
) {}
private validationErrorMessages: Record<string, string> = { private validationErrorMessages: Record<string, string> = {
required: "This field is required", required: 'This field is required',
pattern: "This must be a valid url", pattern: 'This must be a valid url',
}; };
updateErrorMessages(): void { updateErrorMessages(): void {
this.errorMessages = {}; this.errorMessages = {};
Object.keys(this.createLinkForm.controls).forEach(field => { Object.keys(this.createLinkForm.controls).forEach((field) => {
const control = this.createLinkForm.get(field); const control = this.createLinkForm.get(field);
if (control && control.errors) { if (control && control.errors) {
this.errorMessages[field] = Object.keys(control.errors) this.errorMessages[field] = Object.keys(control.errors)
.map(errorKey => this.validationErrorMessages[errorKey] || `Unknown error: ${errorKey}`) .map(
(errorKey) =>
this.validationErrorMessages[errorKey] ||
`Unknown error: ${errorKey}`,
)
.join(' '); .join(' ');
} }
}); });
@ -48,7 +67,12 @@ export class CreateLinkComponent {
ngOnInit(): void { ngOnInit(): void {
this.createLinkForm = new FormGroup({ this.createLinkForm = new FormGroup({
name: new FormControl('', Validators.required), name: new FormControl('', Validators.required),
link: new FormControl('', [Validators.required, Validators.pattern(/((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/)]), link: new FormControl('', [
Validators.required,
Validators.pattern(
/((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/,
),
]),
}); });
this.createLinkForm.valueChanges.subscribe(() => { this.createLinkForm.valueChanges.subscribe(() => {
this.updateErrorMessages(); this.updateErrorMessages();
@ -56,16 +80,18 @@ export class CreateLinkComponent {
} }
submit() { submit() {
if (!this.createLinkForm.valid) { if (!this.createLinkForm.valid) {
return; return;
} }
this.requestFailed = false; this.requestFailed = false;
this.linkService.createLink({ this.linkService
.createLink({
name: this.createLinkForm.get('name')?.value, name: this.createLinkForm.get('name')?.value,
link: this.createLinkForm.get('link')?.value, link: this.createLinkForm.get('link')?.value,
}).catch(() => this.requestFailed = true).finally(() => { })
.catch(() => (this.requestFailed = true))
.finally(() => {
if (!this.requestFailed) { if (!this.requestFailed) {
this.router.navigate(['dashboard']); this.router.navigate(['dashboard']);
} }

View file

@ -1,12 +1,18 @@
<div class="mx-auto container"> <div class="mx-auto container">
<app-navbar></app-navbar> <app-navbar></app-navbar>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<button mat-flat-button color="secondary" class="my-3" (click)="createLink()">Create new Link</button> <button
mat-flat-button
color="secondary"
class="my-3"
(click)="createLink()"
>
Create new Link
</button>
<mat-card appearance="outlined"> <mat-card appearance="outlined">
<mat-card-content> <mat-card-content>
<table mat-table [dataSource]="links" class="mat-elevation-z8"> <table mat-table [dataSource]="links" class="mat-elevation-z8">
<!--- Note that these columns can be defined in any order. <!--- Note that these columns can be defined in any order.
The actual rendered columns are set as a property on the row definition" --> The actual rendered columns are set as a property on the row definition" -->
@ -14,7 +20,7 @@
<ng-container matColumnDef="id"> <ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef>ID</th> <th mat-header-cell *matHeaderCellDef>ID</th>
<td mat-cell *matCellDef="let element"> <td mat-cell *matCellDef="let element">
<a href="{{element.id}}"> <a (click)="goToLink(element)" class="link">
{{ element.id }} {{ element.id }}
</a> </a>
</td> </td>
@ -36,19 +42,31 @@
<ng-container matColumnDef="shortLink"> <ng-container matColumnDef="shortLink">
<th mat-header-cell *matHeaderCellDef>Short link</th> <th mat-header-cell *matHeaderCellDef>Short link</th>
<td mat-cell *matCellDef="let element"> <td mat-cell *matCellDef="let element">
<button mat-flat-button class="my-3" (click)="copyLink(element.id)">Copy short link</button> <button
mat-flat-button
class="my-3"
(click)="copyLink(element.id)"
>
Copy short link
</button>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>Actions</th> <th mat-header-cell *matHeaderCellDef>Actions</th>
<td mat-cell *matCellDef="let element"> <td mat-cell *matCellDef="let element">
<button mat-flat-button color="warn" (click)="deleteLink(element.id)">Delete</button> <button
mat-flat-button
color="warn"
(click)="deleteLink(element)"
>
Delete
</button>
</td> </td>
</ng-container> </ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table> </table>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>

View file

@ -8,9 +8,8 @@ describe('DashboardComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [DashboardComponent] imports: [DashboardComponent],
}) }).compileComponents();
.compileComponents();
fixture = TestBed.createComponent(DashboardComponent); fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View file

@ -7,29 +7,54 @@ import { MatRow, MatTableModule } from '@angular/material/table';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { Link } from '../models/link'; import { Link } from '../models/link';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { ConfirmationModalComponent } from '../confirmation-modal/confirmation-modal.component';
import { MatSnackBar } from '@angular/material/snack-bar';
@Component({ @Component({
selector: 'app-dashboard', selector: 'app-dashboard',
imports: [NavbarComponent, MatTableModule, MatCardModule, MatButtonModule], imports: [NavbarComponent, MatTableModule, MatCardModule, MatButtonModule],
templateUrl: './dashboard.component.html', templateUrl: './dashboard.component.html',
styleUrl: './dashboard.component.css' styleUrl: './dashboard.component.css',
}) })
export class DashboardComponent { export class DashboardComponent {
public links: Link[] = []; public links: Link[] = [];
displayedColumns: string[] = ['id', 'name', 'link', 'shortLink', 'actions']; displayedColumns: string[] = ['id', 'name', 'link', 'shortLink', 'actions'];
constructor(private linkService: LinkService, private router: Router) { }; constructor(
private linkService: LinkService,
private router: Router,
private dialog: MatDialog,
private snackBar: MatSnackBar,
) {}
copyLink(id: string) { copyLink(id: string) {
navigator.clipboard.writeText(this.getShortLink(id)); navigator.clipboard.writeText(this.getShortLink(id));
} }
getShortLink(id: string): string { getShortLink(id: string): string {
return "https://" + window.location.hostname + '/' + id; return 'https://' + window.location.hostname + '/' + id;
}
goToLink(link: Link) {
this.dialog
.open(ConfirmationModalComponent, {
data: {
title: 'Are you sure?',
description:
'Are you sure that you want to open ' + link.link + ' now?',
},
})
.afterClosed()
.subscribe((accepted: Boolean) => {
if (accepted) {
this.router.navigate([link.id]);
}
});
} }
ngOnInit(): void { ngOnInit(): void {
this.linkService.getLinks().then(links => { this.linkService.getLinks().then((links) => {
this.links = links; this.links = links;
}); });
} }
@ -38,10 +63,36 @@ export class DashboardComponent {
this.router.navigate(['create-link']); this.router.navigate(['create-link']);
} }
deleteLink(id: string) { deleteLink(link: Link) {
this.linkService.deleteLink(id); // TODO Check if something went wrong this.dialog
this.links = this.links.filter(link => { .open(ConfirmationModalComponent, {
return link.id != id; data: {
title: 'Are you sure?',
description:
'Are you sure that you want to delete ' + link.name + '?',
},
})
.afterClosed()
.subscribe((accepted: boolean) => {
if (accepted) {
this.linkService.deleteLink(link.id).catch(() => {
const errorNotification = this.snackBar.open(
'Something went wrong.',
);
setTimeout(() => {
errorNotification.dismiss();
}, 5000);
});
this.links = this.links.filter((givenLink) => {
return givenLink.id != link.id;
});
const notification = this.snackBar.open(link.name + ' was deleted.');
setTimeout(() => {
notification.dismiss();
}, 2000);
}
}); });
} }
} }

View file

@ -1,8 +1,15 @@
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8"> <div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm"> <div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img class="mx-auto h-10 w-auto" src="https://tailwindui.com/plus/img/logos/mark.svg?color=indigo&shade=600" <img
alt="Your Company"> class="mx-auto h-10 w-auto"
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Sign in to your account</h2> src="https://tailwindui.com/plus/img/logos/mark.svg?color=indigo&shade=600"
alt="Your Company"
/>
<h2
class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900"
>
Sign in to your account
</h2>
</div> </div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm"> <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
@ -10,33 +17,58 @@
@if (invalidCredentials) { @if (invalidCredentials) {
<div class="mt-2"> <div class="mt-2">
<p <p
class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-red-500 outline outline-red-500 outline-2 -outline-offset-2 sm:text-sm/6"> class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-red-500 outline outline-red-500 outline-2 -outline-offset-2 sm:text-sm/6"
Invalid Credentials</p> >
Invalid Credentials
</p>
</div> </div>
} }
<div> <div>
<label for="email" class="block text-sm/6 font-medium text-gray-900">Email address</label> <label for="email" class="block text-sm/6 font-medium text-gray-900"
>Email address</label
>
<div class="mt-2"> <div class="mt-2">
<input formControlName="email" type="email" name="email" id="email" autocomplete="email" required <input
class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6"> formControlName="email"
type="email"
name="email"
id="email"
autocomplete="email"
required
class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6"
/>
</div> </div>
</div> </div>
<div> <div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<label for="password" class="block text-sm/6 font-medium text-gray-900">Password</label> <label
for="password"
class="block text-sm/6 font-medium text-gray-900"
>Password</label
>
</div> </div>
<div class="mt-2"> <div class="mt-2">
<input formControlName="password" type="password" name="password" id="password" <input
autocomplete="current-password" required formControlName="password"
class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6"> type="password"
name="password"
id="password"
autocomplete="current-password"
required
class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6"
/>
</div> </div>
</div> </div>
<div> <div>
<button (click)="submit()" type="submit" <button
class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Sign (click)="submit()"
in</button> type="submit"
class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>
Sign in
</button>
</div> </div>
</form> </form>
</div> </div>

View file

@ -8,9 +8,8 @@ describe('LoginComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [LoginComponent] imports: [LoginComponent],
}) }).compileComponents();
.compileComponents();
fixture = TestBed.createComponent(LoginComponent); fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View file

@ -8,14 +8,14 @@ import { Router } from '@angular/router';
selector: 'app-login', selector: 'app-login',
imports: [ReactiveFormsModule], imports: [ReactiveFormsModule],
templateUrl: './login.component.html', templateUrl: './login.component.html',
styleUrl: './login.component.css' styleUrl: './login.component.css',
}) })
export class LoginComponent { export class LoginComponent {
public loginForm!: FormGroup; public loginForm!: FormGroup;
public invalidCredentials = false; public invalidCredentials = false;
private pb = new PocketBase(environment.POCKETBASE); private pb = new PocketBase(environment.POCKETBASE);
constructor(private router: Router) { }; constructor(private router: Router) {}
ngOnInit(): void { ngOnInit(): void {
this.loginForm = new FormGroup({ this.loginForm = new FormGroup({
@ -29,12 +29,16 @@ export class LoginComponent {
} }
submit() { submit() {
this.pb.collection("users").authWithPassword( this.pb
.collection('users')
.authWithPassword(
this.loginForm.get('email')?.value, this.loginForm.get('email')?.value,
this.loginForm.get('password')?.value this.loginForm.get('password')?.value,
).then(() => { )
.then(() => {
this.router.navigate(['dashboard']); this.router.navigate(['dashboard']);
}).catch(() => { })
.catch(() => {
this.invalidCredentials = true; this.invalidCredentials = true;
}); });
} }

View file

@ -0,0 +1,4 @@
export interface ConfirmationModalDto {
title: string;
description: string;
}

View file

@ -8,9 +8,8 @@ describe('NavbarComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [NavbarComponent] imports: [NavbarComponent],
}) }).compileComponents();
.compileComponents();
fixture = TestBed.createComponent(NavbarComponent); fixture = TestBed.createComponent(NavbarComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View file

@ -7,6 +7,4 @@ import { Component, ViewEncapsulation } from '@angular/core';
styleUrl: './navbar.component.css', styleUrl: './navbar.component.css',
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
}) })
export class NavbarComponent { export class NavbarComponent {}
}

View file

@ -5,20 +5,26 @@ import {
GuardResult, GuardResult,
MaybeAsync, MaybeAsync,
Router, Router,
RouterStateSnapshot RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import PocketBase from 'pocketbase'; import PocketBase from 'pocketbase';
import { environment } from '../../environments/environment'; import { environment } from '../../environments/environment';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class AuthGuard implements CanActivate { export class AuthGuard implements CanActivate {
constructor(private router: Router) { }; constructor(private router: Router) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> { async canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
): Promise<boolean> {
const pb = new PocketBase(environment.POCKETBASE); const pb = new PocketBase(environment.POCKETBASE);
await pb.collection('users').authRefresh().catch(() => this.router.navigate([''])); await pb
.collection('users')
.authRefresh()
.catch(() => this.router.navigate(['']));
if (pb.authStore.isValid) { if (pb.authStore.isValid) {
return true; return true;

View file

@ -1,11 +1,10 @@
import { Injectable } from "@angular/core"; import { Injectable } from '@angular/core';
import { environment } from "../../environments/environment"; import { environment } from '../../environments/environment';
import PocketBase, { RecordModel } from 'pocketbase'; import PocketBase, { RecordModel } from 'pocketbase';
import { Link } from "../models/link"; import { Link } from '../models/link';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class LinkService { export class LinkService {
private pb = new PocketBase(environment.POCKETBASE); private pb = new PocketBase(environment.POCKETBASE);
@ -18,15 +17,15 @@ export class LinkService {
return this.pb.collection('links').getOne<Link>(id); return this.pb.collection('links').getOne<Link>(id);
} }
deleteLink(id: string) { deleteLink(id: string): Promise<boolean> {
this.pb.collection('links').delete(id); return this.pb.collection('links').delete(id);
} }
createLink(link: any): Promise<RecordModel> { createLink(link: any): Promise<RecordModel> {
return this.pb.collection('links').create({ return this.pb.collection('links').create({
'name': link.name, name: link.name,
'link': link.link, link: link.link,
'owner': this.pb.authStore.record?.id, owner: this.pb.authStore.record?.id,
}); });
} }
} }

View file

@ -1,3 +1,6 @@
<p>Your are being redirected.</p> <p>Your are being redirected.</p>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-5944102294748899" <script
crossorigin="anonymous"></script> async
src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-5944102294748899"
crossorigin="anonymous"
></script>

View file

@ -8,9 +8,8 @@ describe('ViewLinkComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [ViewLinkComponent] imports: [ViewLinkComponent],
}) }).compileComponents();
.compileComponents();
fixture = TestBed.createComponent(ViewLinkComponent); fixture = TestBed.createComponent(ViewLinkComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View file

@ -6,13 +6,19 @@ import { ActivatedRoute, Router } from '@angular/router';
selector: 'app-view-link', selector: 'app-view-link',
imports: [], imports: [],
templateUrl: './view-link.component.html', templateUrl: './view-link.component.html',
styleUrl: './view-link.component.css' styleUrl: './view-link.component.css',
}) })
export class ViewLinkComponent { export class ViewLinkComponent {
constructor(private linkService: LinkService, private route: ActivatedRoute, private router: Router) { } constructor(
private linkService: LinkService,
private route: ActivatedRoute,
private router: Router,
) {}
ngOnInit(): void { ngOnInit(): void {
this.linkService.getLink(this.route.snapshot.params['link']).then(link => { this.linkService
.getLink(this.route.snapshot.params['link'])
.then((link) => {
window.location.href = link.link; window.location.href = link.link;
}); });
} }

View file

@ -1,3 +1,3 @@
export const environment = { export const environment = {
POCKETBASE: 'https://jklink-pocketbase-test.intern.kjan.de' POCKETBASE: 'https://jklink-pocketbase-test.intern.kjan.de',
}; };

View file

@ -1,3 +1,3 @@
export const environment = { export const environment = {
POCKETBASE: 'https://jklink-pocketbase-test.intern.kjan.de' POCKETBASE: 'https://jklink-pocketbase-test.intern.kjan.de',
}; };

View file

@ -1,13 +1,19 @@
<!doctype html> <!doctype html>
<html class="h-full" lang="en" data-theme="light"> <html class="h-full" lang="en" data-theme="light">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<title>Jklink</title> <title>Jklink</title>
<base href="/"> <base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="icon" type="image/x-icon" href="favicon.ico" />
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet"> <link
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet"
/>
</head> </head>
<body class="h-full base-100 mat-typography"> <body class="h-full base-100 mat-typography">
<app-root></app-root> <app-root></app-root>

View file

@ -2,5 +2,6 @@ import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config'; import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component'; import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig) bootstrapApplication(AppComponent, appConfig).catch((err) =>
.catch((err) => console.error(err)); console.error(err),
);

View file

@ -1,4 +1,4 @@
@use '@angular/material' as mat; @use "@angular/material" as mat;
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@ -11,6 +11,10 @@ html {
@include mat.color-variants-backwards-compatibility($theme); @include mat.color-variants-backwards-compatibility($theme);
} }
.link {
@apply underline text-blue-500 cursor-pointer;
}
html, html,
body { body {
height: 100%; height: 100%;

View file

@ -1,8 +1,6 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
content: [ content: ["./src/**/*.{html,ts}"],
"./src/**/*.{html,ts}",
],
theme: { theme: {
extend: {}, extend: {},
}, },
@ -10,5 +8,4 @@ module.exports = {
preflight: false, preflight: false,
}, },
plugins: [], plugins: [],
} };

View file

@ -6,10 +6,6 @@
"outDir": "./out-tsc/app", "outDir": "./out-tsc/app",
"types": [] "types": []
}, },
"files": [ "files": ["src/main.ts"],
"src/main.ts" "include": ["src/**/*.d.ts"]
],
"include": [
"src/**/*.d.ts"
]
} }

View file

@ -4,12 +4,7 @@
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./out-tsc/spec", "outDir": "./out-tsc/spec",
"types": [ "types": ["jasmine"]
"jasmine"
]
}, },
"include": [ "include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
} }