feat: Add custom slug support for links
This commit is contained in:
parent
ab5dbc047c
commit
8e1832416b
15 changed files with 54 additions and 40 deletions
|
@ -25,7 +25,7 @@ describe('AppComponent', () => {
|
|||
fixture.detectChanges();
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('h1')?.textContent).toContain(
|
||||
'Hello, jklink',
|
||||
'Hello, jklink'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,7 +21,7 @@ export const routes: Routes = [
|
|||
canActivate: [AuthGuard],
|
||||
},
|
||||
{
|
||||
path: ':link',
|
||||
path: ':slug',
|
||||
component: ViewLinkComponent,
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
<h2 mat-dialog-title>{{ data.title }}</h2>
|
||||
<mat-dialog-content>{{ data.description }}</mat-dialog-content>
|
||||
<mat-dialog-actions>
|
||||
<button [style.backgroundColor]="'#FFFFFF'" [style.color]="'#000000'" mat-stroked-button color="warn" (click)="accept()">Yes</button>
|
||||
<button
|
||||
[style.backgroundColor]="'#FFFFFF'"
|
||||
[style.color]="'#000000'"
|
||||
mat-stroked-button
|
||||
color="warn"
|
||||
(click)="accept()"
|
||||
>
|
||||
Yes
|
||||
</button>
|
||||
<button mat-flat-button (click)="close()">No</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
<div class="mx-auto container">
|
||||
<app-navbar></app-navbar>
|
||||
<button [style.backgroundColor]="'#FFFFFF'" [style.color]="'#000000'" mat-stroked-button class="mt-3" color="warn" (click)="back()">
|
||||
<button
|
||||
[style.backgroundColor]="'#FFFFFF'"
|
||||
[style.color]="'#000000'"
|
||||
mat-stroked-button
|
||||
class="mt-3"
|
||||
color="warn"
|
||||
(click)="back()"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<mat-card class="mt-3 p-3" appearance="outlined">
|
||||
|
@ -20,6 +27,11 @@
|
|||
placeholder="https://kjan.de"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-error>{{ errorMessages["slug"] }}</mat-error>
|
||||
<mat-label>Custom slug (optional)</mat-label>
|
||||
<input formControlName="slug" matInput />
|
||||
</mat-form-field>
|
||||
<button mat-flat-button type="submit" (click)="submit()">Create</button>
|
||||
</form>
|
||||
</mat-card>
|
||||
|
|
|
@ -32,10 +32,7 @@ export class CreateLinkComponent {
|
|||
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',
|
||||
|
@ -53,7 +50,7 @@ export class CreateLinkComponent {
|
|||
.map(
|
||||
(errorKey) =>
|
||||
this.validationErrorMessages[errorKey] ||
|
||||
`Unknown error: ${errorKey}`,
|
||||
`Unknown error: ${errorKey}`
|
||||
)
|
||||
.join(' ');
|
||||
}
|
||||
|
@ -70,9 +67,10 @@ export class CreateLinkComponent {
|
|||
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]*))?)/,
|
||||
/((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/
|
||||
),
|
||||
]),
|
||||
slug: new FormControl(''),
|
||||
});
|
||||
this.createLinkForm.valueChanges.subscribe(() => {
|
||||
this.updateErrorMessages();
|
||||
|
@ -89,6 +87,7 @@ export class CreateLinkComponent {
|
|||
.createLink({
|
||||
name: this.createLinkForm.get('name')?.value,
|
||||
link: this.createLinkForm.get('link')?.value,
|
||||
slug: this.createLinkForm.get('slug')?.value,
|
||||
})
|
||||
.catch(() => (this.requestFailed = true))
|
||||
.finally(() => {
|
||||
|
|
|
@ -24,11 +24,11 @@
|
|||
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>
|
||||
<ng-container matColumnDef="slug">
|
||||
<th mat-header-cell *matHeaderCellDef>Slug</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<a (click)="goToLink(element)" class="link">
|
||||
{{ element.id }}
|
||||
{{ element.slug }}
|
||||
</a>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
@ -49,11 +49,7 @@
|
|||
<ng-container matColumnDef="shortLink">
|
||||
<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)"
|
||||
>
|
||||
<button mat-flat-button class="my-3" (click)="copyLink(element)">
|
||||
Copy short link
|
||||
</button>
|
||||
</td>
|
||||
|
|
|
@ -27,7 +27,7 @@ import { AuthRecord } from 'pocketbase';
|
|||
})
|
||||
export class DashboardComponent {
|
||||
public links: Link[] = [];
|
||||
displayedColumns: string[] = ['id', 'name', 'link', 'shortLink', 'actions'];
|
||||
displayedColumns: string[] = ['slug', 'name', 'link', 'shortLink', 'actions'];
|
||||
public user!: AuthRecord;
|
||||
|
||||
constructor(
|
||||
|
@ -35,15 +35,15 @@ export class DashboardComponent {
|
|||
private router: Router,
|
||||
private dialog: MatDialog,
|
||||
private snackBar: MatSnackBar,
|
||||
private userService: UserService,
|
||||
private userService: UserService
|
||||
) {}
|
||||
|
||||
copyLink(id: string) {
|
||||
navigator.clipboard.writeText(this.getShortLink(id));
|
||||
copyLink(link: Link) {
|
||||
navigator.clipboard.writeText(this.getShortLink(link));
|
||||
}
|
||||
|
||||
getShortLink(id: string): string {
|
||||
return 'https://' + window.location.hostname + '/' + id;
|
||||
getShortLink(link: Link): string {
|
||||
return 'https://' + window.location.hostname + '/' + link.slug;
|
||||
}
|
||||
|
||||
goToLink(link: Link) {
|
||||
|
@ -91,7 +91,7 @@ export class DashboardComponent {
|
|||
if (accepted) {
|
||||
this.linkService.deleteLink(link.id).catch(() => {
|
||||
const errorNotification = this.snackBar.open(
|
||||
'Something went wrong.',
|
||||
'Something went wrong.'
|
||||
);
|
||||
setTimeout(() => {
|
||||
errorNotification.dismiss();
|
||||
|
|
|
@ -35,10 +35,7 @@ export class LoginComponent {
|
|||
private pb = new PocketBase(environment.POCKETBASE);
|
||||
public errorMessages: Record<string, string> = {};
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private snackBar: MatSnackBar,
|
||||
) {}
|
||||
constructor(private router: Router, private snackBar: MatSnackBar) {}
|
||||
|
||||
private validationErrorMessages: Record<string, string> = {
|
||||
required: 'This field is required',
|
||||
|
@ -55,7 +52,7 @@ export class LoginComponent {
|
|||
.map(
|
||||
(errorKey) =>
|
||||
this.validationErrorMessages[errorKey] ||
|
||||
`Unknown error: ${errorKey}`,
|
||||
`Unknown error: ${errorKey}`
|
||||
)
|
||||
.join(' ');
|
||||
}
|
||||
|
@ -105,7 +102,7 @@ export class LoginComponent {
|
|||
.collection('users')
|
||||
.authWithPassword(
|
||||
this.loginForm.get('email')?.value,
|
||||
this.loginForm.get('password')?.value,
|
||||
this.loginForm.get('password')?.value
|
||||
)
|
||||
.then(() => {
|
||||
this.router.navigate(['dashboard']);
|
||||
|
|
|
@ -3,4 +3,5 @@ export interface Link {
|
|||
link: string;
|
||||
name: string;
|
||||
owner: string;
|
||||
slug: string;
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ export class AuthGuard implements CanActivate {
|
|||
|
||||
async canActivate(
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot,
|
||||
state: RouterStateSnapshot
|
||||
): Promise<boolean> {
|
||||
const pb = new PocketBase(environment.POCKETBASE);
|
||||
await pb
|
||||
|
|
|
@ -13,8 +13,8 @@ export class LinkService {
|
|||
return this.pb.collection<Link>('links').getFullList<Link>();
|
||||
}
|
||||
|
||||
getLink(id: string): Promise<Link> {
|
||||
return this.pb.collection('links').getOne<Link>(id);
|
||||
getLink(slug: string): Promise<Link> {
|
||||
return this.pb.collection('links').getFirstListItem(`slug="${slug}"`);
|
||||
}
|
||||
|
||||
deleteLink(id: string): Promise<boolean> {
|
||||
|
@ -26,6 +26,7 @@ export class LinkService {
|
|||
name: link.name,
|
||||
link: link.link,
|
||||
owner: this.pb.authStore.record?.id,
|
||||
slug: link.slug,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Injectable } from "@angular/core";
|
||||
import { Injectable } from '@angular/core';
|
||||
import PocketBase, { AuthRecord } from 'pocketbase';
|
||||
import { environment } from "../../environments/environment";
|
||||
import { environment } from '../../environments/environment';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
|
|
|
@ -12,12 +12,12 @@ export class ViewLinkComponent {
|
|||
constructor(
|
||||
private linkService: LinkService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private router: Router
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.linkService
|
||||
.getLink(this.route.snapshot.params['link'])
|
||||
.getLink(this.route.snapshot.params['slug'])
|
||||
.then((link) => {
|
||||
window.location.href = link.link;
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html class="h-full" lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
|
|
@ -3,5 +3,5 @@ import { appConfig } from './app/app.config';
|
|||
import { AppComponent } from './app/app.component';
|
||||
|
||||
bootstrapApplication(AppComponent, appConfig).catch((err) =>
|
||||
console.error(err),
|
||||
console.error(err)
|
||||
);
|
||||
|
|
Loading…
Add table
Reference in a new issue