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

Reviewed-on: #6
This commit is contained in:
Jan Gleytenhoover 2025-01-21 10:26:36 +00:00
commit 5b8efc8921
25 changed files with 385 additions and 98 deletions

View file

@ -0,0 +1,32 @@
name: Release
on:
push:
branches:
- prod
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
permissions:
contents: read # for checkout
concurrency:
group: ${{ github.ref }} # Ensure each branch has its own group
cancel-in-progress: false # Prevent new runs from canceling in-progress runs
jobs:
release:
name: Release
runs-on: ubuntu-latest
permissions:
contents: write # to be able to publish a GitHub release
issues: write # to be able to comment on released issues
pull-requests: write # to be able to comment on released pull requests
id-token: write # to enable use of OIDC for npm provenance
steps:
- name: Create Release
uses: https://git.kjan.de/actions/semantic-release@main
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}

View file

@ -27,7 +27,8 @@
} }
], ],
"styles": [ "styles": [
"src/styles.css" "@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.scss"
], ],
"scripts": [] "scripts": []
}, },
@ -97,6 +98,7 @@
} }
], ],
"styles": [ "styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.css" "src/styles.css"
], ],
"scripts": [] "scripts": []

41
package-lock.json generated
View file

@ -9,10 +9,12 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@angular/animations": "^19.0.0", "@angular/animations": "^19.0.0",
"@angular/cdk": "^19.1.0",
"@angular/common": "^19.0.0", "@angular/common": "^19.0.0",
"@angular/compiler": "^19.0.0", "@angular/compiler": "^19.0.0",
"@angular/core": "^19.0.0", "@angular/core": "^19.0.0",
"@angular/forms": "^19.0.0", "@angular/forms": "^19.0.0",
"@angular/material": "^19.1.0",
"@angular/platform-browser": "^19.0.0", "@angular/platform-browser": "^19.0.0",
"@angular/platform-browser-dynamic": "^19.0.0", "@angular/platform-browser-dynamic": "^19.0.0",
"@angular/router": "^19.0.0", "@angular/router": "^19.0.0",
@ -390,6 +392,23 @@
} }
} }
}, },
"node_modules/@angular/cdk": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.1.0.tgz",
"integrity": "sha512-h7VSaMA/vFHb7u1bwoHKl3L3mZLIcXNZw6v7Nei9ITfEo1PfSKbrYhleeqpNikzE+LxNDKJrbZtpAckSYHblmA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
},
"optionalDependencies": {
"parse5": "^7.1.2"
},
"peerDependencies": {
"@angular/common": "^19.0.0 || ^20.0.0",
"@angular/core": "^19.0.0 || ^20.0.0",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/cli": { "node_modules/@angular/cli": {
"version": "19.1.2", "version": "19.1.2",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.1.2.tgz", "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.1.2.tgz",
@ -517,6 +536,24 @@
"rxjs": "^6.5.3 || ^7.4.0" "rxjs": "^6.5.3 || ^7.4.0"
} }
}, },
"node_modules/@angular/material": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/@angular/material/-/material-19.1.0.tgz",
"integrity": "sha512-LTQBWtuRGjNpA7ceQu9PyiUUq0KLfBP8LvL04hLFbEsZ0fIPQ/OO/Otn67/7TMtnHRFnPeezYPHcAHBhiNlR4A==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/animations": "^19.0.0 || ^20.0.0",
"@angular/cdk": "19.1.0",
"@angular/common": "^19.0.0 || ^20.0.0",
"@angular/core": "^19.0.0 || ^20.0.0",
"@angular/forms": "^19.0.0 || ^20.0.0",
"@angular/platform-browser": "^19.0.0 || ^20.0.0",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/platform-browser": { "node_modules/@angular/platform-browser": {
"version": "19.1.1", "version": "19.1.1",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.1.1.tgz", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.1.1.tgz",
@ -6663,7 +6700,7 @@
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"dev": true, "devOptional": true,
"engines": { "engines": {
"node": ">=0.12" "node": ">=0.12"
}, },
@ -10387,7 +10424,7 @@
"version": "7.2.1", "version": "7.2.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz",
"integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==",
"dev": true, "devOptional": true,
"dependencies": { "dependencies": {
"entities": "^4.5.0" "entities": "^4.5.0"
}, },

View file

@ -11,10 +11,12 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^19.0.0", "@angular/animations": "^19.0.0",
"@angular/cdk": "^19.1.0",
"@angular/common": "^19.0.0", "@angular/common": "^19.0.0",
"@angular/compiler": "^19.0.0", "@angular/compiler": "^19.0.0",
"@angular/core": "^19.0.0", "@angular/core": "^19.0.0",
"@angular/forms": "^19.0.0", "@angular/forms": "^19.0.0",
"@angular/material": "^19.1.0",
"@angular/platform-browser": "^19.0.0", "@angular/platform-browser": "^19.0.0",
"@angular/platform-browser-dynamic": "^19.0.0", "@angular/platform-browser-dynamic": "^19.0.0",
"@angular/router": "^19.0.0", "@angular/router": "^19.0.0",
@ -41,4 +43,4 @@
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"typescript": "~5.6.2" "typescript": "~5.6.2"
} }
} }

View file

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

View file

@ -2,6 +2,8 @@ import { Routes } from '@angular/router';
import { LoginComponent } from './login/login.component'; import { LoginComponent } from './login/login.component';
import { DashboardComponent } from './dashboard/dashboard.component'; import { DashboardComponent } from './dashboard/dashboard.component';
import { AuthGuard } from './service/auth.service'; import { AuthGuard } from './service/auth.service';
import { CreateLinkComponent } from './create-link/create-link.component';
import { ViewLinkComponent } from './view-link/view-link.component';
export const routes: Routes = [ export const routes: Routes = [
{ {
@ -13,6 +15,15 @@ export const routes: Routes = [
component: DashboardComponent, component: DashboardComponent,
canActivate: [AuthGuard], canActivate: [AuthGuard],
}, },
{
path: 'create-link',
component: CreateLinkComponent,
canActivate: [AuthGuard],
},
{
path: ':link',
component: ViewLinkComponent,
},
{ {
path: "**", path: "**",
redirectTo: "", redirectTo: "",

View file

@ -0,0 +1,19 @@
<div class="mx-auto container">
<app-navbar></app-navbar>
<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-label>Name</mat-label>
<input formControlName="name" matInput placeholder="My Awesome link">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-error>{{errorMessages['link']}}</mat-error>
<mat-label>Link</mat-label>
<input formControlName="link" matInput type="url" placeholder="https://kjan.de">
</mat-form-field>
<button mat-flat-button type="submit" (click)="submit()">Create</button>
</form>
</mat-card>
</div>

View file

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

View file

@ -0,0 +1,74 @@
import { Component } from '@angular/core';
import { NavbarComponent } from '../navbar/navbar.component';
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';
import { MatInputModule, MatLabel } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatButtonModule } from '@angular/material/button';
import { debounceTime } from 'rxjs';
@Component({
selector: 'app-create-link',
imports: [NavbarComponent, ReactiveFormsModule, MatCardModule, MatFormFieldModule, MatInputModule, MatButtonModule],
templateUrl: './create-link.component.html',
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) { }
private validationErrorMessages: Record<string, string> = {
required: "This field is required",
pattern: "This must be a valid url",
};
updateErrorMessages(): void {
this.errorMessages = {};
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}`)
.join(' ');
}
});
}
back() {
this.router.navigate(['dashboard']);
}
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]*))?)/)]),
});
this.createLinkForm.valueChanges.subscribe(() => {
this.updateErrorMessages();
});
}
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']);
}
});
}
}

View file

@ -1,27 +1,56 @@
<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">
<table class="table"> <button mat-flat-button color="secondary" class="my-3" (click)="createLink()">Create new Link</button>
<!-- head -->
<thead> <mat-card appearance="outlined">
<tr> <mat-card-content>
<th></th> <table mat-table [dataSource]="links" class="mat-elevation-z8">
<th>Name</th>
<th>Url</th> <!--- Note that these columns can be defined in any order.
<th>Short Url</th> The actual rendered columns are set as a property on the row definition" -->
</tr>
</thead> <!-- Position Column -->
<tbody> <ng-container matColumnDef="id">
<!-- row 1 --> <th mat-header-cell *matHeaderCellDef> ID </th>
@for (link of links; track link) { <td mat-cell *matCellDef="let element">
<tr class="bg-base-200"> <a href="{{element.id}}">
<th>{{link.id}}</th> {{element.id}}
<td>{{link.name}}</td> </a>
<td><a href="{{link.link}}">{{link.link}}</a></td> </td>
<td>ndy</td> </ng-container>
</tr>
} <!-- Name Column -->
</tbody> <ng-container matColumnDef="name">
</table> <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>
</ng-container>
<!-- Symbol Column -->
<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)">Copy short link</button>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> Actions </th>
<td mat-cell *matCellDef="let element">
<button mat-flat-button color="warn" (click)="deleteLink(element.id)">Delete</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</mat-card-content>
</mat-card>
</div> </div>
</div> </div>

View file

@ -2,21 +2,46 @@ import { Component } from '@angular/core';
import { LinkService } from '../service/link.service'; import { LinkService } from '../service/link.service';
import { RecordModel } from 'pocketbase'; import { RecordModel } from 'pocketbase';
import { NavbarComponent } from '../navbar/navbar.component'; import { NavbarComponent } from '../navbar/navbar.component';
import { Router, RouterLink } from '@angular/router';
import { MatRow, MatTableModule } from '@angular/material/table';
import { MatCardModule } from '@angular/material/card';
import { Link } from '../models/link';
import { MatButtonModule } from '@angular/material/button';
@Component({ @Component({
selector: 'app-dashboard', selector: 'app-dashboard',
imports: [NavbarComponent], 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: any[] = []; public links: Link[] = [];
displayedColumns: string[] = ['id', 'name', 'link', 'shortLink', 'actions'];
constructor(private linkService: LinkService) { }; constructor(private linkService: LinkService, private router: Router) { };
copyLink(id: string) {
navigator.clipboard.writeText(this.getShortLink(id));
}
getShortLink(id: string): string {
return window.location.hostname + '/' + id;
}
ngOnInit(): void { ngOnInit(): void {
this.linkService.getLinks().then(links => { this.linkService.getLinks().then(links => {
this.links = links; this.links = links;
}); });
} }
createLink() {
this.router.navigate(['create-link']);
}
deleteLink(id: string) {
this.linkService.deleteLink(id); // TODO Check if something went wrong
this.links = this.links.filter(link => {
return link.id != id;
});
}
} }

6
src/app/models/link.ts Normal file
View file

@ -0,0 +1,6 @@
export interface Link {
id: string;
link: string;
name: string;
owner: string;
}

View file

@ -1,52 +0,0 @@
<div class="navbar bg-base-100 rounded-2xl shadow-xl my-3 z-100">
<div class="navbar-start">
<div class="dropdown">
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden">
<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="M4 6h16M4 12h8m-8 6h16" />
</svg>
</div>
<ul
tabindex="0"
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-[10000] mt-3 w-52 p-2 shadow">
<li><a>Dashboard</a></li>
<li>
<a>Parent</a>
<ul class="p-2">
<li><a>Submenu 1</a></li>
<li><a>Submenu 2</a></li>
</ul>
</li>
<li><a>Item 3</a></li>
</ul>
</div>
<a class="btn btn-ghost text-xl">jklink</a>
</div>
<div class="navbar-center hidden lg:flex">
<ul class="menu menu-horizontal px-1">
<li><a>Item 1</a></li>
<li>
<details>
<summary>Parent</summary>
<ul class="p-2">
<li><a>Submenu 1</a></li>
<li><a>Submenu 2</a></li>
</ul>
</details>
</li>
<li><a>Item 3</a></li>
</ul>
</div>
<div class="navbar-end">
<a class="btn">Button</a>
</div>
</div>

View file

@ -1,6 +1,7 @@
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";
@Injectable({ @Injectable({
@ -9,7 +10,23 @@ import PocketBase, { RecordModel } from 'pocketbase';
export class LinkService { export class LinkService {
private pb = new PocketBase(environment.POCKETBASE); private pb = new PocketBase(environment.POCKETBASE);
getLinks(): Promise<RecordModel[]> { getLinks(): Promise<Link[]> {
return this.pb.collection('links').getFullList(); return this.pb.collection<Link>('links').getFullList<Link>();
}
getLink(id: string): Promise<Link> {
return this.pb.collection('links').getOne<Link>(id);
}
deleteLink(id: string) {
this.pb.collection('links').delete(id);
}
createLink(link: any): Promise<RecordModel> {
return this.pb.collection('links').create({
'name': link.name,
'link': link.link,
'owner': this.pb.authStore.record?.id,
});
} }
} }

View file

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

View file

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

View file

@ -0,0 +1,19 @@
import { Component } from '@angular/core';
import { LinkService } from '../service/link.service';
import { ActivatedRoute, Router } from '@angular/router';
@Component({
selector: 'app-view-link',
imports: [],
templateUrl: './view-link.component.html',
styleUrl: './view-link.component.css'
})
export class ViewLinkComponent {
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;
});
}
}

View file

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

View file

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

View file

@ -6,8 +6,10 @@
<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 href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head> </head>
<body class="h-full base-100" > <body class="h-full base-100 mat-typography" >
<app-root></app-root> <app-root></app-root>
</body> </body>
</html> </html>

View file

@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

22
src/styles.scss Normal file
View file

@ -0,0 +1,22 @@
@use '@angular/material' as mat;
@tailwind base;
@tailwind components;
@tailwind utilities;
$theme: mat.define-theme();
html {
@include mat.all-component-themes($theme);
// This line allows you to use color="..." for your toolbar.
@include mat.color-variants-backwards-compatibility($theme);
}
html,
body {
height: 100%;
}
body {
margin: 0;
font-family: Roboto, "Helvetica Neue", sans-serif;
}

View file

@ -6,14 +6,9 @@ module.exports = {
theme: { theme: {
extend: {}, extend: {},
}, },
daisyui: { corePlugins: {
styled: true, preflight: false,
themes: true,
base: true,
utils: true,
logs: true,
rtl: false
}, },
plugins: [require("@tailwindcss/typography"), require("daisyui")], plugins: [],
} }