style: format code and add prettier configuration files

This commit is contained in:
Jan K9f 2025-01-21 13:29:09 +01:00
commit 395c1bc75f
Signed by: jank
GPG key ID: 50620ADD22CD330B
39 changed files with 338 additions and 207 deletions

View file

@ -24,6 +24,8 @@ describe('AppComponent', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
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',
imports: [RouterOutlet],
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';
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,
},
{
path: "**",
redirectTo: "",
path: '**',
redirectTo: '',
},
];

View file

@ -1,5 +1,5 @@
<h2 mat-dialog-title>{{data.title}}</h2>
<mat-dialog-content>{{data.description}}</mat-dialog-content>
<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>

View file

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

View file

@ -12,9 +12,14 @@ import { ConfirmationModalDto } from '../models/confirmation-modal';
@Component({
selector: 'app-confirmation-modal',
imports: [MatDialogTitle, MatDialogContent, MatDialogActions, MatButtonModule],
imports: [
MatDialogTitle,
MatDialogContent,
MatDialogActions,
MatButtonModule,
],
templateUrl: './confirmation-modal.component.html',
styleUrl: './confirmation-modal.component.css'
styleUrl: './confirmation-modal.component.css',
})
export class ConfirmationModalComponent {
public data: ConfirmationModalDto = inject(MAT_DIALOG_DATA);

View file

@ -1,17 +1,24 @@
<div class="mx-auto container">
<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">
<form class="flex flex-col" [formGroup]="createLinkForm">
<mat-form-field appearance="outline">
<mat-error>{{errorMessages['name']}}</mat-error>
<mat-error>{{ errorMessages["name"] }}</mat-error>
<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 appearance="outline">
<mat-error>{{errorMessages['link']}}</mat-error>
<mat-error>{{ errorMessages["link"] }}</mat-error>
<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>
<button mat-flat-button type="submit" (click)="submit()">Create</button>
</form>

View file

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

View file

@ -1,6 +1,11 @@
import { Component } from '@angular/core';
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 { Router } from '@angular/router';
import { MatCardModule } from '@angular/material/card';
@ -11,31 +16,45 @@ import { debounceTime } from 'rxjs';
@Component({
selector: 'app-create-link',
imports: [NavbarComponent, ReactiveFormsModule, MatCardModule, MatFormFieldModule, MatInputModule, MatButtonModule],
imports: [
NavbarComponent,
ReactiveFormsModule,
MatCardModule,
MatFormFieldModule,
MatInputModule,
MatButtonModule,
],
templateUrl: './create-link.component.html',
styleUrl: './create-link.component.css'
styleUrl: './create-link.component.css',
})
export class CreateLinkComponent {
public createLinkForm!: FormGroup;
public requestFailed: boolean = false;
public errorMessages: Record<string, string> = {};
constructor(private linkService: LinkService, private router: Router) { }
constructor(
private linkService: LinkService,
private router: Router,
) {}
private validationErrorMessages: Record<string, string> = {
required: "This field is required",
pattern: "This must be a valid url",
required: 'This field is required',
pattern: 'This must be a valid url',
};
updateErrorMessages(): void {
this.errorMessages = {};
Object.keys(this.createLinkForm.controls).forEach(field => {
Object.keys(this.createLinkForm.controls).forEach((field) => {
const control = this.createLinkForm.get(field);
if (control && 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(' ');
}
});
@ -48,7 +67,12 @@ export class CreateLinkComponent {
ngOnInit(): void {
this.createLinkForm = new FormGroup({
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.updateErrorMessages();
@ -56,19 +80,21 @@ export class CreateLinkComponent {
}
submit() {
if (!this.createLinkForm.valid) {
return;
}
this.requestFailed = false;
this.linkService.createLink({
name: this.createLinkForm.get('name')?.value,
link: this.createLinkForm.get('link')?.value,
}).catch(() => this.requestFailed = true).finally(() => {
if (!this.requestFailed) {
this.router.navigate(['dashboard']);
}
});
this.linkService
.createLink({
name: this.createLinkForm.get('name')?.value,
link: this.createLinkForm.get('link')?.value,
})
.catch(() => (this.requestFailed = true))
.finally(() => {
if (!this.requestFailed) {
this.router.navigate(['dashboard']);
}
});
}
}

View file

@ -1,54 +1,72 @@
<div class="mx-auto container">
<app-navbar></app-navbar>
<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-content>
<table mat-table [dataSource]="links" class="mat-elevation-z8">
<!--- Note that these columns can be defined in any order.
The actual rendered columns are set as a property on the row definition" -->
<!-- Position Column -->
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef> ID </th>
<th mat-header-cell *matHeaderCellDef>ID</th>
<td mat-cell *matCellDef="let element">
<a (click)="goToLink(element)" class="link">
{{element.id}}
{{ element.id }}
</a>
</td>
</ng-container>
<!-- Name Column -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> Name </th>
<td mat-cell *matCellDef="let element"> {{element.name}} </td>
<th mat-header-cell *matHeaderCellDef>Name</th>
<td mat-cell *matCellDef="let element">{{ element.name }}</td>
</ng-container>
<!-- Weight Column -->
<ng-container matColumnDef="link">
<th mat-header-cell *matHeaderCellDef> Link </th>
<td mat-cell *matCellDef="let element"> {{element.link}} </td>
<th mat-header-cell *matHeaderCellDef>Link</th>
<td mat-cell *matCellDef="let element">{{ element.link }}</td>
</ng-container>
<!-- Symbol Column -->
<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">
<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>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> Actions </th>
<th mat-header-cell *matHeaderCellDef>Actions</th>
<td mat-cell *matCellDef="let element">
<button mat-flat-button color="warn" (click)="deleteLink(element)">Delete</button>
<button
mat-flat-button
color="warn"
(click)="deleteLink(element)"
>
Delete
</button>
</td>
</ng-container>
<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>
</mat-card-content>
</mat-card>

View file

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

View file

@ -15,40 +15,48 @@ import { MatSnackBar } from '@angular/material/snack-bar';
selector: 'app-dashboard',
imports: [NavbarComponent, MatTableModule, MatCardModule, MatButtonModule],
templateUrl: './dashboard.component.html',
styleUrl: './dashboard.component.css'
styleUrl: './dashboard.component.css',
})
export class DashboardComponent {
public links: Link[] = [];
displayedColumns: string[] = ['id', 'name', 'link', 'shortLink', 'actions'];
constructor(private linkService: LinkService, private router: Router, private dialog: MatDialog, private snackBar: MatSnackBar) { };
constructor(
private linkService: LinkService,
private router: Router,
private dialog: MatDialog,
private snackBar: MatSnackBar,
) {}
copyLink(id: string) {
navigator.clipboard.writeText(this.getShortLink(id));
}
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]);
}
});
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 {
this.linkService.getLinks().then(links => {
this.linkService.getLinks().then((links) => {
this.links = links;
});
}
createLink() {
@ -56,29 +64,35 @@ export class DashboardComponent {
}
deleteLink(link: Link) {
this.dialog.open(ConfirmationModalComponent, {
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.");
this.dialog
.open(ConfirmationModalComponent, {
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(() => {
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);
}
});
notification.dismiss();
}, 2000);
}
});
}
}

View file

@ -1,42 +1,74 @@
<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">
<img class="mx-auto h-10 w-auto" 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>
<img
class="mx-auto h-10 w-auto"
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 class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<form [formGroup]="loginForm" class="space-y-6" action="#" method="POST">
@if (invalidCredentials) {
<div class="mt-2">
<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">
Invalid Credentials</p>
</div>
<div class="mt-2">
<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"
>
Invalid Credentials
</p>
</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">
<input 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">
<input
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 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 class="mt-2">
<input formControlName="password" 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">
<input
formControlName="password"
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>
<button (click)="submit()" 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>
<button
(click)="submit()"
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>
</form>
</div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,20 +5,26 @@ import {
GuardResult,
MaybeAsync,
Router,
RouterStateSnapshot
RouterStateSnapshot,
} from '@angular/router';
import PocketBase from 'pocketbase';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
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);
await pb.collection('users').authRefresh().catch(() => this.router.navigate(['']));
await pb
.collection('users')
.authRefresh()
.catch(() => this.router.navigate(['']));
if (pb.authStore.isValid) {
return true;

View file

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

View file

@ -1,3 +1,6 @@
<p>Your are being redirected.</p>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-5944102294748899"
crossorigin="anonymous"></script>
<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 () => {
await TestBed.configureTestingModule({
imports: [ViewLinkComponent]
})
.compileComponents();
imports: [ViewLinkComponent],
}).compileComponents();
fixture = TestBed.createComponent(ViewLinkComponent);
component = fixture.componentInstance;

View file

@ -6,14 +6,20 @@ import { ActivatedRoute, Router } from '@angular/router';
selector: 'app-view-link',
imports: [],
templateUrl: './view-link.component.html',
styleUrl: './view-link.component.css'
styleUrl: './view-link.component.css',
})
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 {
this.linkService.getLink(this.route.snapshot.params['link']).then(link => {
window.location.href = link.link;
});
this.linkService
.getLink(this.route.snapshot.params['link'])
.then((link) => {
window.location.href = link.link;
});
}
}