Fix frontpage and pin functionality (#11)

Co-authored-by: Myx <info@azaaxin.com>
This commit is contained in:
2025-01-25 05:57:18 +01:00
committed by GitHub
parent 7e60850c7a
commit 28e55a5a6b
31 changed files with 372 additions and 60 deletions

View File

@@ -20,6 +20,7 @@
"@ng-icons/core": "^29.5.1",
"@ng-icons/css.gg": "^29.5.1",
"@ng-icons/heroicons": "^29.5.1",
"@ng-icons/material-icons": "^30.2.0",
"@primeng/themes": "^19.0.5",
"angularx-qrcode": "^18.0.2",
"primeicons": "^7.0.0",
@@ -3924,6 +3925,14 @@
"tslib": "^2.2.0"
}
},
"node_modules/@ng-icons/material-icons": {
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@ng-icons/material-icons/-/material-icons-30.2.0.tgz",
"integrity": "sha512-xteBKL7648UoB3CWYFYSSoh3TzwicM/qi9Yh/QNRMwThqCzE1DPpcjOT7CsnO/nfNnu0gAWsLJwdPNT26hhrvw==",
"dependencies": {
"tslib": "^2.3.0"
}
},
"node_modules/@ngtools/webpack": {
"version": "19.1.4",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-19.1.4.tgz",

View File

@@ -22,6 +22,7 @@
"@ng-icons/core": "^29.5.1",
"@ng-icons/css.gg": "^29.5.1",
"@ng-icons/heroicons": "^29.5.1",
"@ng-icons/material-icons": "^30.2.0",
"@primeng/themes": "^19.0.5",
"angularx-qrcode": "^18.0.2",
"primeicons": "^7.0.0",

View File

@@ -1,4 +1,4 @@
import { Component } from '@angular/core';
import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core';
import { HeaderComponent } from './header/header.component';
import { FooterComponent } from './footer/footer.component';
import { RouterOutlet } from '@angular/router';

View File

@@ -10,8 +10,14 @@ import { WordCounterComponent } from '../tools/client-side/word-counter/word-cou
import { ColorPickerComponent } from '../tools/client-side/color-picker/color-picker.component';
import { QrCodeGeneratorComponent } from '../tools/client-side/qr-code-generator/qr-code-generator.component';
import { OracleGuidConverterComponent } from '../tools/client-side/oracle-guid-converter/oracle-guid-converter.component';
import { FrontPageComponent } from './front-page/front-page.component';
export const routes: Routes = [
{
path: '',
pathMatch: 'full',
component: FrontPageComponent
},
{
path: 'ascii-to-text',
pathMatch: 'full',
@@ -66,6 +72,9 @@ export const routes: Routes = [
path: 'oracle-guid-converter',
pathMatch: 'full',
component: OracleGuidConverterComponent
}, {
path: '**',
redirectTo: ''
}
];

View File

@@ -0,0 +1,27 @@
<div class="landing-page-container">
<p-card header="Welcome to Bytefy DevTools Hub">
<p>
<strong>Bytefy DevTools Hub</strong> is a website designed to help developers and media creators
with a variety of useful tools. Whether you're working on web development, media editing, or managing your
projects, we have you covered.
</p>
<p>
Our tools include:
</p>
<p-divider></p-divider>
<div class="matrix-wrapper">
<div *ngFor="let tool of tools" class="tool-item">
<h4>{{ tool.name }}</h4>
<p>{{ tool.description }}</p>
<p-button label="Learn More" icon="pi pi-arrow-right" class="p-button-outlined" [routerLink]="tool.link"></p-button>
</div>
</div>
<p-divider></p-divider>
<div>
<h3>Privacy Information</h3>
<p>
All of our tools operate offline and do not store any data, unless explicitly stated on the tool's page. We are committed to respecting your privacy and ensuring the security of your information.
</p>
</div>
</p-card>
</div>

View File

@@ -0,0 +1,54 @@
.landing-page-container {
display: flex;
justify-content: center;
padding: 2rem;
}
.tool-item {
margin: 1.5rem 0;
width: 31%;
}
@media only screen and (min-device-width: 480px) {
.tool-item {
width: 100%;
}
}
@media only screen and (min-width:768px) {
.tool-item {
width: 47%;
}
}
.tool-item h4 {
color: var(--p-button-primary-border-color);
}
p-card {
max-width: 1140px;
.p-card {
background: none !important;
background-color: none !important;
}
}
::ng-deep .p-card {
background: none !important;
background-color: none !important;
}
p-divider {
margin: 1.5rem 0;
}
p-button {
margin-top: 1rem;
}
.matrix-wrapper {
display: flex;
flex-wrap: wrap;
gap: 25px;
}

View File

@@ -0,0 +1,29 @@
import { Component } from '@angular/core';
import { ButtonModule } from 'primeng/button';
import { DividerModule } from 'primeng/divider';
import { CardModule } from 'primeng/card';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';
@Component({
selector: 'app-default',
templateUrl: './default.component.html',
styleUrls: ['./default.component.scss'],
imports: [
CommonModule,
ButtonModule,
CardModule,
DividerModule,
RouterLink
],
})
export class DefaultComponent {
tools = [
{ name: 'Image Converter', description: 'Convert images between various formats.', link: '/image-converter' },
{ name: 'GUID Generator', description: 'Generate unique GUIDs for your projects.', link: '/guid' },
{ name: 'Color Picker', description: 'Pick and preview colors for web design.', link: '/color-picker' },
{ name: 'JWT Reader', description: 'Decode and view JWT tokens.', link: '/jwt-decoder' },
{ name: 'Cron Job Expression Creator', description: 'Create cron expressions for scheduling jobs.', link: '/text-to-cron' },
{ name: 'QR Code Generator', description: 'Generate custom QR codes, from text or files.', link: '/qr-code-generator' },
];
}

View File

@@ -0,0 +1,6 @@
@if(hasPins) {
<app-dynamic-loader [paths]="pinnedPaths"></app-dynamic-loader>
}
@else {
<app-default></app-default>
}

View File

@@ -0,0 +1,14 @@
import { Component } from '@angular/core';
import { DynamicLoaderComponent } from '../shared/dynamic-loader/dynamic-loader.component';
import { DefaultComponent } from './default/default.component';
@Component({
selector: 'app-front-page',
templateUrl: './front-page.component.html',
styleUrls: ['./front-page.component.scss'],
imports: [DynamicLoaderComponent, DefaultComponent]
})
export class FrontPageComponent {
pinnedPaths: string[] = JSON.parse(localStorage.getItem('pinnedPaths') || '[]');
hasPins: boolean = this.pinnedPaths.length > 0;
}

View File

@@ -1,5 +1,5 @@
<p-megamenu [model]="items">
<ng-template pTemplate="start">
<img class="logotype" src="../../assets/logo-full-orange-beta-vectorized.svg" alt="Bytefy Logotype" />
<img class="logotype" src="../../assets/logo-full-orange-beta-vectorized.svg" [routerLink]="['/']" alt="Bytefy Logotype" />
</ng-template>
</p-megamenu>

View File

@@ -1,6 +1,9 @@
.logotype {
width: 140px;
}
img {
cursor: pointer;
}
::ng-deep .p-megamenu-col-12 {
flex-direction: row !important;

View File

@@ -1,28 +0,0 @@
/* tslint:disable:no-unused-variable */
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { HeaderComponent } from './header.component';
describe('HeaderComponent', () => {
let component: HeaderComponent;
let fixture: ComponentFixture<HeaderComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ HeaderComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HeaderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -4,12 +4,13 @@ import { MegaMenuModule } from 'primeng/megamenu';
import { ButtonModule } from 'primeng/button';
import { CommonModule } from '@angular/common';
import { AvatarModule } from 'primeng/avatar';
import { RouterLink } from '@angular/router';
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss'],
imports: [MegaMenuModule, ButtonModule, CommonModule, AvatarModule]
imports: [MegaMenuModule, ButtonModule, CommonModule, AvatarModule, RouterLink]
})
export class HeaderComponent implements OnInit {
items: MegaMenuItem[] | undefined;

View File

@@ -20,4 +20,10 @@
[value]="bottomValue"
[placeholder]="bottomPlaceholder">
</textarea>
<div class="space" *ngIf="privacyText !== ''">
<h3>Privacy information</h3>
<p>
{{ privacyText }}
</p>
</div>
</page>

View File

@@ -6,7 +6,6 @@
flex-direction: row;
justify-content: center;
padding: 5px;
// background-color: var(--primary-contrast);
i {
transform: rotate(90deg);
@@ -39,12 +38,3 @@ textarea {
resize: none;
background-color: var(--primary-contrast);
}
::ng-deep .p-panel-header {
justify-content: unset !important;
* {
margin-right: 5px;
}
}

View File

@@ -6,6 +6,7 @@ import { TextareaModule } from 'primeng/textarea';
import { PanelModule } from 'primeng/panel';
import { TagModule } from 'primeng/tag';
import { PageComponent } from '../page/page.component';
import { CardModule } from 'primeng/card';
@Component({
selector: 'app-dual-textarea',
@@ -18,7 +19,8 @@ import { PageComponent } from '../page/page.component';
PanelModule,
CommonModule,
TagModule,
PageComponent
PageComponent,
CardModule
]
})
export class DualTextareaComponent {
@@ -30,6 +32,8 @@ export class DualTextareaComponent {
@Input() topValue: string = '';
@Input() bottomValue: string = '';
@Input() isBeta: boolean = false;
@Input() privacyText: string = '';
@Output() topChange = new EventEmitter<string>();
@Output() bottomChange = new EventEmitter<string>();

View File

@@ -0,0 +1 @@
<ng-container #dynamicContainer></ng-container>

View File

@@ -0,0 +1,42 @@
import {
Component,
ViewChild,
ViewContainerRef,
ComponentFactoryResolver,
Type,
Input,
OnChanges,
SimpleChanges,
} from '@angular/core';
import { DynamicLoaderService } from './dynamic-loader.service';
@Component({
selector: 'app-dynamic-loader',
templateUrl: './dynamic-loader.component.html',
})
export class DynamicLoaderComponent implements OnChanges {
@ViewChild('dynamicContainer', { read: ViewContainerRef, static: true })
container!: ViewContainerRef;
@Input() paths: string[] = [];
constructor(private dynamicComponentService: DynamicLoaderService) {}
ngOnChanges(changes: SimpleChanges): void {
if (changes['paths']) {
this.loadComponents();
}
}
private loadComponents() {
this.container.clear();
const components: Type<any>[] =
this.dynamicComponentService.getComponentsByPaths(this.paths);
components.forEach((component) => {
this.container.createComponent(component);
});
}
}

View File

@@ -0,0 +1,16 @@
/* tslint:disable:no-unused-variable */
import { TestBed, inject } from '@angular/core/testing';
import { DynamicLoaderService } from './dynamic-loader.service';
describe('Service: DynamicLoader', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [DynamicLoaderService]
});
});
it('should ...', inject([DynamicLoaderService], (service: DynamicLoaderService) => {
expect(service).toBeTruthy();
}));
});

View File

@@ -0,0 +1,31 @@
import { Injectable, Type } from '@angular/core';
import { routes } from '../../app.routes';
@Injectable({
providedIn: 'root',
})
export class DynamicLoaderService {
private componentLookupTable: Record<string, Type<any>> = {};
constructor() {
this.buildLookupTable();
}
private buildLookupTable() {
routes.forEach((route) => {
if (route.path && route.component) {
this.componentLookupTable[route.path] = route.component;
}
});
}
getComponentByPath(path: string): Type<any> | undefined {
return this.componentLookupTable[path];
}
getComponentsByPaths(paths: string[]): Type<any>[] {
return paths
.map((path) => this.componentLookupTable[path])
.filter((component): component is Type<any> => !!component);
}
}

View File

@@ -1,5 +1,13 @@
<div class="card">
<p-panel [header]="header">
<p-panel>
<ng-template pTemplate="header">
<span class="p-panel-title">{{header}}</span>
</ng-template>
<ng-template #icons *ngIf="!isRoot">
<p-button class="pinButton" severity="secondary" [pTooltip]="isPinned ? 'Unpin from start page.' : 'Pin to start page.'" rounded text (click)="togglePin()">
<ng-icon [name]="displayIcon"></ng-icon>
</p-button>
</ng-template>
<ng-content></ng-content>
</p-panel>
</div>

View File

@@ -10,11 +10,15 @@
width: 1140px;
}
::ng-deep .p-panel-header {
justify-content: unset !important;
.pinButton {
margin-right: unset !important;
}
* {
margin-right: 5px;
::ng-deep .p-panel-header {
justify-content: space-between !important;
}
}
:host(.root-path) {
width: unset;
}

View File

@@ -1,12 +1,74 @@
import { Component, Input, OnInit } from '@angular/core';
import { Component, ElementRef, Input, OnInit, Renderer2 } from '@angular/core';
import { ButtonModule } from 'primeng/button';
import { PanelModule } from 'primeng/panel';
import { MenuModule } from 'primeng/menu';
import { NgIconComponent, provideIcons } from '@ng-icons/core';
import { matPushPinOutline } from '@ng-icons/material-icons/outline'
import { matPushPinSharp } from '@ng-icons/material-icons/sharp'
import { Router } from '@angular/router';
import { CommonModule } from '@angular/common';
import { TooltipModule } from 'primeng/tooltip';
@Component({
selector: 'page',
templateUrl: './page.component.html',
styleUrls: ['./page.component.scss'],
imports: [PanelModule]
imports: [
PanelModule,
ButtonModule,
MenuModule,
NgIconComponent,
CommonModule,
TooltipModule
],
viewProviders: [provideIcons({ matPushPinOutline, matPushPinSharp })]
})
export class PageComponent {
export class PageComponent implements OnInit{
@Input() header: string = '';
@Input() pinId: Number = 0;
@Input() pathId ='';
isRoot = false;
isPinned: boolean = false;
currentPath: string = '';
displayIcon: string = 'matPushPinOutline';
constructor(private router: Router, private renderer: Renderer2, private el: ElementRef) {
this.currentPath = this.router.url.split('?')[0].replace("/", "");
}
ngOnInit(): void {
if (this.isRootPath()) {
this.renderer.addClass(this.el.nativeElement, 'root-path');
} else {
this.renderer.removeClass(this.el.nativeElement, 'root-path');
}
this.isPinned = JSON.parse(localStorage.getItem('pinnedPaths') || '[]').includes(this.currentPath);
this.displayIcon = this.isPinned ? 'matPushPinSharp' : 'matPushPinOutline';
}
togglePin(): void {
this.isPinned = !this.isPinned;
this.displayIcon = this.isPinned ? 'matPushPinSharp' : 'matPushPinOutline';
const pinnedPaths = JSON.parse(localStorage.getItem('pinnedPaths') || '[]');
if (this.isPinned) {
if (!pinnedPaths.includes(this.currentPath)) {
pinnedPaths.push(this.currentPath);
}
} else {
const index = pinnedPaths.indexOf(this.currentPath);
if (index !== -1) {
pinnedPaths.splice(index, 1);
}
}
localStorage.setItem('pinnedPaths', JSON.stringify(pinnedPaths));
}
isRootPath(): boolean {
this.isRoot = this.currentPath === '';
return this.isRoot;
}
}

View File

@@ -71,4 +71,11 @@
</tr>
</ng-template>
</p-table>
<div class="space" *ngIf="privacyText !== ''">
<p-card header="Privacy information">
<p>
{{ privacyText }}
</p>
</p-card>
</div>
</page>

View File

@@ -10,6 +10,8 @@ import { BadgeModule } from 'primeng/badge';
import { HttpHeaders } from '@angular/common/http';
import { TagModule } from 'primeng/tag';
import { PageComponent } from '../page/page.component';
import { Divider } from 'primeng/divider';
import { CardModule } from 'primeng/card';
interface ProcessedFile {
name: string;
@@ -31,7 +33,8 @@ interface ProcessedFile {
AutoCompleteModule,
BadgeModule,
TagModule,
PageComponent
PageComponent,
CardModule
]
})
export class FileConverterComponent implements OnInit {
@@ -44,6 +47,7 @@ export class FileConverterComponent implements OnInit {
selectedFile: File[] | null = null;
@Output() fileSelected = new EventEmitter<File[]>();
@Input() privacyText: string = '';
@Input() isBeta: boolean = false;
@Input() filteredFiles: string[] = [];
@Input() isPreview: boolean = true;

View File

@@ -27,3 +27,7 @@ html {
border: unset !important;
border-radius: unset !important;
}
.space {
margin-top: 10px;
}

View File

@@ -1,5 +1,5 @@
<app-dual-textarea
title="base64 to text"
title="Base64 to Text"
topPlaceholder="Enter base64 here..."
bottomPlaceholder="Text will appear here..."
[topValue]="convertedBase64"

View File

@@ -1,6 +1,7 @@
<app-file-converter
title="Image converter"
method="post"
privacyText="This tool requires internet access to convert images. The images are processed on the servers and are not stored on any disk."
[isPreview]="false"
[fileTypeSelector]="true"
[processedFiles]="processedFiles"

View File

@@ -8,12 +8,20 @@ import { CommonModule } from '@angular/common';
import { FileConverterComponent } from "../../../app/shared/upload/file-converter.component";
import { Format, ProcessedFile } from '../../../app/models/conversion.model';
import { HttpHeaders } from '@angular/common/http';
import { CardModule } from 'primeng/card';
@Component({
selector: 'app-image-converter',
templateUrl: 'image-converter.component.html',
styleUrls: ['image-converter.component.scss'],
imports: [DropdownModule, AutoCompleteModule, FormsModule, CommonModule, FileConverterComponent]
imports: [
DropdownModule,
AutoCompleteModule,
FormsModule,
CommonModule,
FileConverterComponent,
CardModule
]
})
export class ImageConverterComponent implements OnInit, OnDestroy {
constructor(private ImageService: ImageService) { }
@@ -66,7 +74,6 @@ export class ImageConverterComponent implements OnInit, OnDestroy {
}
}
onFormatSelected(format: string) {
this.selectedFormat = format;
}