import { Component, ViewEncapsulation, ViewChild, NgZone, OnInit, ElementRef, ChangeDetectorRef, AfterViewInit, Renderer2, Inject, PLATFORM_ID, effect, } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { Title, Meta, DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { DOCUMENT, isPlatformBrowser } from '@angular/common'; declare global { interface Window { adsbygoogle: any[]; } } import { ActivatedRoute, RouterOutlet } from '@angular/router'; import debounce from 'lodash/debounce' import { MatSidenav, MatSidenavModule } from '@angular/material/sidenav' import { RouterService, } from '../modules/web'; import * as emojiRegex from 'emoji-regex' import {LocaleService, SettingsService} from '../modules/web'; import {NotifyService} from '../modules/material'; import {extractStars, extractTitle} from '../utils/extrac-title'; import {extractTitleWithStars} from '../utils/extrac-title'; import {isMobile} from '../utils/is-mobile'; import twemoji from 'twemoji' import {environment} from "../../environments/environment"; import { MatButtonModule } from '@angular/material/button'; import { Footer } from './footer/cory-layout-footer'; import { MatTooltipModule } from '@angular/material/tooltip'; import { Status } from '../component/cory-web-pages-build-status'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatMenuModule } from '@angular/material/menu'; import { Header } from './header/cory-layout-header'; import { Loading } from '../modules/material/component/cory-mat-loading'; //FIXME corifeus - matrix const regexFixCorifeusMatrix = /^(\/)?(corifeus)([^-])(\/)?(.*)/ declare global { interface Window { coryAppWebPagesNavigate: any, coryAppWebPagesNavigateHash: any, } } @Component({ selector: 'cory-layout', templateUrl: 'cory-layout.html', encapsulation: ViewEncapsulation.None, imports: [Loading, Header, MatSidenavModule, MatMenuModule, MatFormFieldModule, MatInputModule, MatIconModule, MatCardModule, Status, MatTooltipModule, RouterOutlet, Footer, MatButtonModule] }) export class Layout implements OnInit, AfterViewInit { private debounceSearchText: Function; menuMenuActive: any; menuRepoActive: any searchText: string; extractTitle = extractTitle; sideNavOpened = false @ViewChild('menuSidenav', {read: MatSidenav, static: true}) public menuSidenav: MatSidenav; @ViewChild('searchText', {read: ElementRef, static: true}) public searchTextInputRefRead: ElementRef; currentRepo: string; body: HTMLElement; i18n: any; config: any; repos: any[]; packages: any; settings: any; packageJson: any = { version: undefined, corifeus: { ['time-stamp']: undefined, code: '', publish: false, } } title: string; icon: string; pageTitleHtml: SafeHtml = ''; public isMobile: boolean = false; constructor( private router: RouterService, private route: ActivatedRoute, protected notify: NotifyService, protected locale: LocaleService, protected settingsAll: SettingsService, private zone: NgZone, private ref: ChangeDetectorRef, private titleService: Title, private metaService: Meta, private renderer: Renderer2, private sanitizer: DomSanitizer, @Inject(DOCUMENT) private document: Document, @Inject(PLATFORM_ID) private platformId: Object, ) { this.body = this.document.getElementsByTagName('body')[0]; this.isMobile = isPlatformBrowser(this.platformId) ? isMobile() : false; this.settings = settingsAll.data.pages; this.currentRepo = this.settings.github.defaultRepo; effect(() => { this.locale.state(); this.i18n = this.locale.data?.pages; }); const paramsSig = toSignal(this.route.params, { initialValue: {} as any }); effect(() => { const params = paramsSig(); const repo = params['repo']; const pathname = isPlatformBrowser(this.platformId) ? location.pathname : (this.document.location?.pathname || '/'); if (repo === 'corifeus' && repo === pathname.slice(1)) { this.navigate('matrix'); return; } this.currentRepo = repo; if (repo === undefined) { this.currentRepo = this.settings.github.defaultRepo; } this.load(); }); } ngOnInit() { this.debounceSearchText = debounce(this.handleSearch, this.settings.debounce.default); } onSidenavOpenedChange(value: boolean) { this.sideNavOpened = value; this.openedChange = value; } onSidenavClosedStart() { this.sideNavOpened = false; } openedChange = false packageMenuOpen() { // this.body.style.overflowY = 'hidden'; // console.log('this.menuSidenav.opened', this.menuSidenav.opened, 'this.openedChange', this.openedChange) if (this.menuSidenav.opened || this.openedChange) { return } this.menuSidenav.open(); setTimeout(() => { if (this.isMobile) { this.searchTextInputRefRead.nativeElement.blur() } // /** const e = this.document.querySelector('.cory-mat-menu-item-active') if (e) { // e.scrollIntoView(true); // const viewportH = Math.max(document.documentElement.clientHeight, window.innerHeight || 0); // window.scrollBy(0, (e.getBoundingClientRect().height-viewportH)/2); e.scrollIntoView({ block: "center", }); } // **/ }, 500) } handleSearch(searchText: string) { this.searchText = searchText.trim(); this.reposSearchCache = undefined; this.reposSearchCacheKey = undefined; } private reposSearchCache: Array = undefined; private reposSearchCacheKey: string = undefined; get reposSearch(): Array { if (this.searchText === '' || this.searchText === undefined) { return this.repos; } if (this.reposSearchCacheKey === this.searchText && this.reposSearchCache !== undefined) { return this.reposSearchCache; } const regexes: Array = []; this.searchText.split(/[\s,]+/).forEach(search => { if (search === '') { return; } regexes.push( new RegExp('.*' + search + '.*', 'i') ) }) this.reposSearchCache = Object.values(this.packages).filter((pkg: any) => { for (let regex of regexes) { if (regex.test(pkg.name) || regex.test(pkg.corifeus.reponame) || regex.test(pkg.corifeus.code)) { return true; } } return false; }).map((pkg: any) => pkg.corifeus.reponame); this.reposSearchCacheKey = this.searchText; return this.reposSearchCache; } async load() { if (this.packages === undefined) { const httpResponse = await fetch(this.settings.p3x.git.url); const response: any = await httpResponse.json(); this.packages = response.repo; let sortedObject = {} sortedObject = Object.keys(this.packages).sort((a, b) => { return (this.packages[b].corifeus.stargazers_count || 0) - (this.packages[a].corifeus.stargazers_count || 0) }).reduce((prev, curr, i) => { prev[i] = this.packages[curr] return prev }, {}) this.packages = {}; Object.keys(sortedObject).forEach(key => { const item = sortedObject[key] if (item.corifeus.prefix !== undefined) { this.packages[item.name.substr(item.corifeus.prefix.length)] = item; } else { this.packages[item.name] = item; } }) this.ref.markForCheck() this.repos = Object.keys(this.packages); } if (!this.packages.hasOwnProperty(this.currentRepo)) { this.currentRepo = 'corifeus'; } this.packageJson = this.packages[this.currentRepo]; this.title = this.packageJson.description; this.icon = this.packageJson.corifeus.icon !== undefined ? `${this.packageJson.corifeus.icon}` : 'fas fa-bolt'; const plainTitle = this.title.replace(emojiRegex.default(), ''); this.titleService.setTitle(plainTitle); const canonicalRepo = this.currentRepo === 'corifeus' ? 'matrix' : this.currentRepo; const canonicalUrl = `https://corifeus.com/${canonicalRepo}`; const description = `${plainTitle} - Open source documentation and packages by Corifeus`; // Update SEO meta tags dynamically this.metaService.updateTag({ name: 'description', content: description }); this.metaService.updateTag({ property: 'og:title', content: plainTitle }); this.metaService.updateTag({ property: 'og:description', content: description }); this.metaService.updateTag({ property: 'og:url', content: canonicalUrl }); this.metaService.updateTag({ property: 'og:type', content: 'article' }); this.metaService.updateTag({ name: 'twitter:title', content: plainTitle }); this.metaService.updateTag({ name: 'twitter:description', content: description }); this.metaService.updateTag({ name: 'twitter:url', content: canonicalUrl }); // Update canonical link tag dynamically this.updateCanonicalUrl(canonicalUrl); // Update JSON-LD structured data this.updateJsonLd(canonicalUrl, plainTitle, description); const noScriptEl = this.document.getElementById('cory-seo'); if (noScriptEl) { noScriptEl.innerHTML = ''; this.repos.forEach((repo: any) => { const a = this.renderer.createElement('a'); this.renderer.setAttribute(a, 'href', `/${repo === 'corifeus' ? 'matrix' : repo}`); a.innerText = repo; this.renderer.appendChild(noScriptEl, a); const a2 = this.renderer.createElement('a'); this.renderer.setAttribute(a2, 'href', `https://github.com/patrikx3/${repo}`); a2.innerText = 'Github ' + repo; this.renderer.appendChild(noScriptEl, a2); }) } if (isPlatformBrowser(this.platformId)) { window.coryAppWebPagesNavigate = (path?: string) => { this.zone.run(() => { if (path && path.includes('#')) { const hashIndex = path.indexOf('#') const pathMainPath = path.substring(0, hashIndex) const hash = path.substring(hashIndex + 1) this.navigate(pathMainPath); window.coryAppWebPagesNavigateHash(hash) } else { this.navigate(path); } }); }; window.coryAppWebPagesNavigateHash = (id: any) => { const scroll = (id: string) => { const el = this.document.getElementById(id); if (el === null) { return; } el.scrollIntoView({ block: "center", }) } if (typeof id === 'string') { const hash = `#${id.replace(/-parent$/, '')}`; if (history.pushState) { history.pushState(null, '', `${location.pathname}${hash}`); } else { location.hash = hash; } scroll(id); } else { id = `${id.id}`; setTimeout(() => { scroll(id) }, 500) } return false; } } this.pageTitleHtml = this.sanitizer.bypassSecurityTrustHtml( this.renderTwemoji(this.packageJson.description), ); } async navigate(path?: string) { if (path === undefined) { path = `${this.currentRepo}/index.html`; } //FIXME corifeus - matrix //console.log(' ') //console.log(path) if (regexFixCorifeusMatrix.test(path)) { path = path.replace(regexFixCorifeusMatrix, 'matrix$3$5') //console.log(1, RegExp.$1, 2, RegExp.$2, 3, RegExp.$3, 4, RegExp.$4, 5, RegExp.$5, 6, RegExp.$6, 7, RegExp.$7) //console.log('match', path) } this.menuMenuActive = ''; //console.log('cory-layout', path); this.router.navigateTop([path]); } isOpenWrt() { return this.packageJson !== undefined && this.packageJson.corifeus !== undefined && this.packageJson.corifeus.hasOwnProperty('type') && this.packageJson.corifeus.type === 'openwrt'; } packageMenuClose() { // this.body.style.overflowY = 'auto'; this.menuSidenav.close(); } search(searchText: string) { this.debounceSearchText(searchText); } renderTwemoji(text: string) { let options if (environment.production) { options = { folder: 'svg', ext: '.svg', base: '/assets/twemoji/', } } else { options = { folder: 'svg', ext: '.svg', base: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/' } } return !text ? text : twemoji.parse(text, options) } keyDownFunction(event: any) { const repos = this.reposSearch; if (event.keyCode == 13 && repos.length === 1) { this.zone.run(() => { const navigate = `/${repos[0]}/index.html` this.debounceSearchText(''); this.searchTextInputRefRead.nativeElement.blur() this.searchTextInputRefRead.nativeElement.value = ''; this.packageMenuClose(); this.navigate(navigate); }); } } get showTitle() { const rawPath = typeof location !== 'undefined' ? location.pathname : (this.document.location?.pathname || '/' + (this.currentRepo || '')); const pathname = rawPath.toLowerCase() const pieces = pathname.split('/') // console.log(pieces) const showTitle = pieces.length === 2 || (pieces.length === 3 && pieces[2] === 'index.html') // const showTitle = pathname.endsWith('index.html') || (!pathname.includes('.') && !pathname.includes('open-collective')); //console.log('showTitle', pathname, showTitle) return showTitle; } extractTitleWithStars(pkg: any) { const title = extractTitleWithStars(pkg); return title; } extractStars(stars: number) { return extractStars(stars) } private updateCanonicalUrl(canonicalUrl: string) { let linkEl = this.document.getElementById('cory-canonical') as HTMLLinkElement | null; if (!linkEl) { linkEl = this.renderer.createElement('link') as HTMLLinkElement; this.renderer.setAttribute(linkEl, 'id', 'cory-canonical'); this.renderer.setAttribute(linkEl, 'rel', 'canonical'); this.renderer.appendChild(this.document.head, linkEl); const legacy = this.document.querySelector('link[rel="canonical"]:not(#cory-canonical)'); if (legacy && legacy.parentNode) { legacy.parentNode.removeChild(legacy); } } this.renderer.setAttribute(linkEl, 'href', canonicalUrl); } private updateJsonLd(canonicalUrl: string, plainTitle: string, description: string) { let scriptEl = this.document.getElementById('cory-jsonld'); if (!scriptEl) { scriptEl = this.renderer.createElement('script'); this.renderer.setAttribute(scriptEl, 'id', 'cory-jsonld'); this.renderer.setAttribute(scriptEl, 'type', 'application/ld+json'); this.renderer.appendChild(this.document.head, scriptEl); } const pkg = this.packageJson || {}; const corifeusMeta = pkg.corifeus || {}; const version: string | undefined = pkg.version; const timeStamp: string | undefined = corifeusMeta['time-stamp']; const stargazers: number | undefined = corifeusMeta.stargazers_count; const code: string | undefined = corifeusMeta.code; const angularVersion: string | undefined = corifeusMeta.angular; const nodeVersion: string | undefined = corifeusMeta.nodejs; const breadcrumbs = { '@type': 'BreadcrumbList', itemListElement: [ { '@type': 'ListItem', position: 1, name: 'Corifeus', item: 'https://corifeus.com/matrix' }, { '@type': 'ListItem', position: 2, name: plainTitle, item: canonicalUrl }, ], }; const softwareSourceCode: any = { '@context': 'https://schema.org', '@type': 'SoftwareSourceCode', name: plainTitle, alternateName: pkg.name, description, url: canonicalUrl, codeRepository: `https://github.com/patrikx3/${this.currentRepo}`, programmingLanguage: 'TypeScript', inLanguage: 'en', license: 'https://opensource.org/licenses/MIT', author: { '@type': 'Person', name: 'Patrik Laszlo', url: 'https://patrikx3.com', sameAs: [ 'https://github.com/patrikx3', 'https://www.npmjs.com/~patrikx3', ], }, publisher: { '@type': 'Organization', name: 'Corifeus', url: 'https://corifeus.com', }, image: 'https://corifeus.com/assets/favicon.ico', keywords: [ 'corifeus', 'patrikx3', 'open source', pkg.name, code, 'angular', 'nodejs', 'typescript', ].filter(Boolean).join(', '), }; if (version) { softwareSourceCode.softwareVersion = version; } if (timeStamp) { softwareSourceCode.dateModified = timeStamp; softwareSourceCode.datePublished = timeStamp; } if (angularVersion || nodeVersion) { const runtime: string[] = []; if (angularVersion) runtime.push(`Angular ${angularVersion}`); if (nodeVersion) runtime.push(`Node ${nodeVersion}`); softwareSourceCode.runtimePlatform = runtime.join(', '); } if (typeof stargazers === 'number' && stargazers > 0) { softwareSourceCode.interactionStatistic = { '@type': 'InteractionCounter', interactionType: 'https://schema.org/LikeAction', userInteractionCount: stargazers, }; } const graph = { '@context': 'https://schema.org', '@graph': [softwareSourceCode, breadcrumbs], }; if (scriptEl) { scriptEl.textContent = JSON.stringify(graph); } } ngAfterViewInit() { /* setTimeout(() => { try { (window.adsbygoogle = window.adsbygoogle || []).push({}); (window.adsbygoogle = window.adsbygoogle || []).push({}); } catch (e) { console.error('AdSense error:', e); } }, 100); // Add a small delay to ensure DOM is rendered */ } }