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 @@ +
+ + + +
+ + {{errorMessages['name']}} + Name + + + + {{errorMessages['link']}} + Link + + + +
+
+
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 @@
- - - - - - - - - - - - - @for (link of links; track link) { - - - - - - - } - -
NameUrlShort Url
{{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: [], }