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
		Add a link
		
	
		Reference in a new issue