diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml
new file mode 100644
index 0000000..f717de7
--- /dev/null
+++ b/.gitea/workflows/release.yml
@@ -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 }}
+
diff --git a/angular.json b/angular.json
index f2cd277..3f41ec4 100644
--- a/angular.json
+++ b/angular.json
@@ -27,7 +27,8 @@
}
],
"styles": [
- "src/styles.css"
+ "@angular/material/prebuilt-themes/azure-blue.css",
+ "src/styles.scss"
],
"scripts": []
},
@@ -97,6 +98,7 @@
}
],
"styles": [
+ "@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.css"
],
"scripts": []
diff --git a/package-lock.json b/package-lock.json
index d6e2024..6a63114 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,10 +9,12 @@
"version": "0.0.0",
"dependencies": {
"@angular/animations": "^19.0.0",
+ "@angular/cdk": "^19.1.0",
"@angular/common": "^19.0.0",
"@angular/compiler": "^19.0.0",
"@angular/core": "^19.0.0",
"@angular/forms": "^19.0.0",
+ "@angular/material": "^19.1.0",
"@angular/platform-browser": "^19.0.0",
"@angular/platform-browser-dynamic": "^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": {
"version": "19.1.2",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.1.2.tgz",
@@ -517,6 +536,24 @@
"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": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.1.1.tgz",
@@ -6663,7 +6700,7 @@
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
- "dev": true,
+ "devOptional": true,
"engines": {
"node": ">=0.12"
},
@@ -10387,7 +10424,7 @@
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz",
"integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==",
- "dev": true,
+ "devOptional": true,
"dependencies": {
"entities": "^4.5.0"
},
diff --git a/package.json b/package.json
index fb2e239..86546d4 100644
--- a/package.json
+++ b/package.json
@@ -11,10 +11,12 @@
"private": true,
"dependencies": {
"@angular/animations": "^19.0.0",
+ "@angular/cdk": "^19.1.0",
"@angular/common": "^19.0.0",
"@angular/compiler": "^19.0.0",
"@angular/core": "^19.0.0",
"@angular/forms": "^19.0.0",
+ "@angular/material": "^19.1.0",
"@angular/platform-browser": "^19.0.0",
"@angular/platform-browser-dynamic": "^19.0.0",
"@angular/router": "^19.0.0",
@@ -41,4 +43,4 @@
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2"
}
-}
+}
\ No newline at end of file
diff --git a/src/app/app.config.ts b/src/app/app.config.ts
index a1e7d6f..96116fd 100644
--- a/src/app/app.config.ts
+++ b/src/app/app.config.ts
@@ -2,7 +2,8 @@ import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
+import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
export const appConfig: ApplicationConfig = {
- providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes)]
+ providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideAnimationsAsync()]
};
diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts
index b66a2af..4c2ca33 100644
--- a/src/app/app.routes.ts
+++ b/src/app/app.routes.ts
@@ -2,6 +2,8 @@ import { Routes } from '@angular/router';
import { LoginComponent } from './login/login.component';
import { DashboardComponent } from './dashboard/dashboard.component';
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 = [
{
@@ -13,6 +15,15 @@ export const routes: Routes = [
component: DashboardComponent,
canActivate: [AuthGuard],
},
+ {
+ path: 'create-link',
+ component: CreateLinkComponent,
+ canActivate: [AuthGuard],
+ },
+ {
+ path: ':link',
+ component: ViewLinkComponent,
+ },
{
path: "**",
redirectTo: "",
diff --git a/src/app/create-link/create-link.component.css b/src/app/create-link/create-link.component.css
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/create-link/create-link.component.html b/src/app/create-link/create-link.component.html
new file mode 100644
index 0000000..6bc479c
--- /dev/null
+++ b/src/app/create-link/create-link.component.html
@@ -0,0 +1,19 @@
+
diff --git a/src/app/create-link/create-link.component.spec.ts b/src/app/create-link/create-link.component.spec.ts
new file mode 100644
index 0000000..b706423
--- /dev/null
+++ b/src/app/create-link/create-link.component.spec.ts
@@ -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;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [CreateLinkComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(CreateLinkComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/create-link/create-link.component.ts b/src/app/create-link/create-link.component.ts
new file mode 100644
index 0000000..76c3dcf
--- /dev/null
+++ b/src/app/create-link/create-link.component.ts
@@ -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 = {};
+
+ constructor(private linkService: LinkService, private router: Router) { }
+
+ private validationErrorMessages: Record = {
+ 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']);
+ }
+ });
+ }
+}
diff --git a/src/app/dashboard/dashboard.component.html b/src/app/dashboard/dashboard.component.html
index d6c8733..d797e70 100644
--- a/src/app/dashboard/dashboard.component.html
+++ b/src/app/dashboard/dashboard.component.html
@@ -1,27 +1,56 @@
-
-
-
-
- |
- Name |
- Url |
- Short Url |
-
-
-
-
- @for (link of links; track link) {
-
- {{link.id}} |
- {{link.name}} |
- {{link.link}} |
- ndy |
-
- }
-
-
+
+
+
+
+
+
+
+
+
+
+ ID |
+
+
+ {{element.id}}
+
+ |
+
+
+
+
+ Name |
+ {{element.name}} |
+
+
+
+
+ Link |
+ {{element.link}} |
+
+
+
+
+ Short link |
+
+
+ |
+
+
+
+ Actions |
+
+
+ |
+
+
+
+
+
+
+
diff --git a/src/app/dashboard/dashboard.component.ts b/src/app/dashboard/dashboard.component.ts
index 09f1a42..428b89f 100644
--- a/src/app/dashboard/dashboard.component.ts
+++ b/src/app/dashboard/dashboard.component.ts
@@ -2,21 +2,46 @@ import { Component } from '@angular/core';
import { LinkService } from '../service/link.service';
import { RecordModel } from 'pocketbase';
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({
selector: 'app-dashboard',
- imports: [NavbarComponent],
+ imports: [NavbarComponent, MatTableModule, MatCardModule, MatButtonModule],
templateUrl: './dashboard.component.html',
styleUrl: './dashboard.component.css'
})
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 {
this.linkService.getLinks().then(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;
+ });
+ }
}
diff --git a/src/app/models/link.ts b/src/app/models/link.ts
new file mode 100644
index 0000000..0f961c3
--- /dev/null
+++ b/src/app/models/link.ts
@@ -0,0 +1,6 @@
+export interface Link {
+ id: string;
+ link: string;
+ name: string;
+ owner: string;
+}
diff --git a/src/app/navbar/navbar.component.html b/src/app/navbar/navbar.component.html
index 1d6d14c..e69de29 100644
--- a/src/app/navbar/navbar.component.html
+++ b/src/app/navbar/navbar.component.html
@@ -1,52 +0,0 @@
-
diff --git a/src/app/service/link.service.ts b/src/app/service/link.service.ts
index 51eb8d8..89b9329 100644
--- a/src/app/service/link.service.ts
+++ b/src/app/service/link.service.ts
@@ -1,6 +1,7 @@
import { Injectable } from "@angular/core";
import { environment } from "../../environments/environment";
import PocketBase, { RecordModel } from 'pocketbase';
+import { Link } from "../models/link";
@Injectable({
@@ -9,7 +10,23 @@ import PocketBase, { RecordModel } from 'pocketbase';
export class LinkService {
private pb = new PocketBase(environment.POCKETBASE);
- getLinks(): Promise {
- return this.pb.collection('links').getFullList();
+ getLinks(): Promise {
+ return this.pb.collection('links').getFullList();
+ }
+
+ getLink(id: string): Promise {
+ return this.pb.collection('links').getOne(id);
+ }
+
+ deleteLink(id: string) {
+ this.pb.collection('links').delete(id);
+ }
+
+ createLink(link: any): Promise {
+ return this.pb.collection('links').create({
+ 'name': link.name,
+ 'link': link.link,
+ 'owner': this.pb.authStore.record?.id,
+ });
}
}
diff --git a/src/app/view-link/view-link.component.css b/src/app/view-link/view-link.component.css
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/view-link/view-link.component.html b/src/app/view-link/view-link.component.html
new file mode 100644
index 0000000..a80c841
--- /dev/null
+++ b/src/app/view-link/view-link.component.html
@@ -0,0 +1,3 @@
+Your are being redirected.
+
diff --git a/src/app/view-link/view-link.component.spec.ts b/src/app/view-link/view-link.component.spec.ts
new file mode 100644
index 0000000..1d509b7
--- /dev/null
+++ b/src/app/view-link/view-link.component.spec.ts
@@ -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;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [ViewLinkComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(ViewLinkComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/view-link/view-link.component.ts b/src/app/view-link/view-link.component.ts
new file mode 100644
index 0000000..21951c3
--- /dev/null
+++ b/src/app/view-link/view-link.component.ts
@@ -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;
+ });
+ }
+}
diff --git a/src/environments/environment.development.ts b/src/environments/environment.development.ts
index 1ef1662..4dd998c 100644
--- a/src/environments/environment.development.ts
+++ b/src/environments/environment.development.ts
@@ -1,3 +1,3 @@
export const environment = {
- POCKETBASE: 'http://pocketbase-yocs0oko0o8cws44kw8gk8g8.192.168.178.105.sslip.io/'
+ POCKETBASE: 'https://jklink-pocketbase-test.intern.kjan.de'
};
diff --git a/src/environments/environment.ts b/src/environments/environment.ts
index 1ef1662..4dd998c 100644
--- a/src/environments/environment.ts
+++ b/src/environments/environment.ts
@@ -1,3 +1,3 @@
export const environment = {
- POCKETBASE: 'http://pocketbase-yocs0oko0o8cws44kw8gk8g8.192.168.178.105.sslip.io/'
+ POCKETBASE: 'https://jklink-pocketbase-test.intern.kjan.de'
};
diff --git a/src/index.html b/src/index.html
index c660faa..5ab9cdc 100644
--- a/src/index.html
+++ b/src/index.html
@@ -6,8 +6,10 @@
+
+
-
+
diff --git a/src/styles.css b/src/styles.css
deleted file mode 100644
index b5c61c9..0000000
--- a/src/styles.css
+++ /dev/null
@@ -1,3 +0,0 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
diff --git a/src/styles.scss b/src/styles.scss
new file mode 100644
index 0000000..2b30ff0
--- /dev/null
+++ b/src/styles.scss
@@ -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;
+}
diff --git a/tailwind.config.js b/tailwind.config.js
index a57d5cf..f24e856 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -6,14 +6,9 @@ module.exports = {
theme: {
extend: {},
},
- daisyui: {
- styled: true,
- themes: true,
- base: true,
- utils: true,
- logs: true,
- rtl: false
+ corePlugins: {
+ preflight: false,
},
- plugins: [require("@tailwindcss/typography"), require("daisyui")],
+ plugins: [],
}