RSS Git Download  Clone
Raw Blame History 20kB 577 lines
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<any> = undefined;
    private reposSearchCacheKey: string = undefined;

    get reposSearch(): Array<any> {
        if (this.searchText === '' || this.searchText === undefined) {
            return this.repos;
        }
        if (this.reposSearchCacheKey === this.searchText && this.reposSearchCache !== undefined) {
            return this.reposSearchCache;
        }
        const regexes: Array<RegExp> = [];
        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
        */
      }
}