Push all to new repo

This commit is contained in:
pedrocx486 2022-12-29 02:29:18 -03:00
commit 40fd1c7bee
60 changed files with 25083 additions and 0 deletions

50
.eslintrc.json Executable file
View File

@ -0,0 +1,50 @@
{
"root": true,
"ignorePatterns": [
"projects/**/*"
],
"overrides": [
{
"files": [
"*.ts"
],
"parserOptions": {
"project": [
"tsconfig.json"
],
"createDefaultProgram": true
},
"extends": [
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "app",
"style": "camelCase"
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "app",
"style": "kebab-case"
}
]
}
},
{
"files": [
"*.html"
],
"extends": [
"plugin:@angular-eslint/template/recommended"
],
"rules": {}
}
]
}

42
.gitignore vendored Normal file
View File

@ -0,0 +1,42 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db

3
.vscode/settings.json vendored Executable file
View File

@ -0,0 +1,3 @@
{
"angular.enable-strict-mode-prompt": false
}

15
LICENSE Normal file
View File

@ -0,0 +1,15 @@
ISC License
Copyright (c) {{ year }}, {{ author }}
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

27
README.md Executable file
View File

@ -0,0 +1,27 @@
# ngx-retroblog
Alternate version of [ngx-dumblog](https://github.com/pedroCX486/ngx-dumblog), with Markdown support! With customization on the settings.json file, lightweight size (no bootstrap!) and more. The editor you need to compile from the [showdown-canvas](https://github.com/pedroCX486/showdown-canvas) project.
## Customizations
On the settings.json file you'll find options to customize your blog title, avatar, bio, social networks (leave the fields empty, don't delete them if you don't want them showing up!) and ability to select how many posts will be shown on the home page.
## Creating and editing posts
You have to compile and host a build of the [showdown-canvas](https://tildegit.org/pedrocx486/showdown-canvas) editor. From there you can create and edit posts. To have those posts reflect on your blog, you need to place the files it generates in the assets/posts folder of your install.
## Running locally
First run `npm install` then run `npm start` and it should open automatically on `http://localhost:4200/`.
## Build
Run `npm run build` to build it. You can also run `custom-build.bat` to build it and then have the index.html title changed after build. But **first** change the variable BLOGNAME inside the .bat file! I intend to add a tool in the future capable of reading the name from the settings.json and then updating the index.html after the build, instead of keeping this .bat file.
## Contributing
Just do a pull request. :-)
## Why?
Because I always thought Jekyll to be too convoluted and I wanted something dumb and simple. The code isn't an example of awesomeness but it does the job and pretty well at that.

105
angular.json Executable file
View File

@ -0,0 +1,105 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"ngx-retroblog": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
},
"@schematics/angular:application": {
"strict": true
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/ngx-retroblog",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "ngx-retroblog:build:production"
},
"development": {
"browserTarget": "ngx-retroblog:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "ngx-retroblog:build"
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
}
}
}
}
},
"defaultProject": "ngx-retroblog",
"cli": {
"defaultCollection": "@angular-eslint/schematics"
}
}

23743
package-lock.json generated Executable file

File diff suppressed because it is too large Load Diff

43
package.json Executable file
View File

@ -0,0 +1,43 @@
{
"name": "ngx-retroblog",
"version": "0.0.1",
"scripts": {
"ng": "ng",
"start": "ng serve --open",
"build": "ng lint && ng build --configuration production",
"build:subfolder": "ng build --base-href=\"/blog/\" --configuration production",
"watch": "ng build --watch --configuration development",
"lint": "ng lint"
},
"private": true,
"dependencies": {
"@angular/animations": "~13.3.0",
"@angular/common": "~13.3.0",
"@angular/compiler": "~13.3.0",
"@angular/core": "~13.3.0",
"@angular/forms": "~13.3.0",
"@angular/platform-browser": "~13.3.0",
"@angular/platform-browser-dynamic": "~13.3.0",
"@angular/router": "~13.3.0",
"rxjs": "~7.5.0",
"showdown": "^2.1.0",
"tslib": "^2.3.0",
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "~13.3.7",
"@angular-eslint/builder": "13.2.1",
"@angular-eslint/eslint-plugin": "13.2.1",
"@angular-eslint/eslint-plugin-template": "13.2.1",
"@angular-eslint/schematics": "13.2.1",
"@angular-eslint/template-parser": "13.2.1",
"@angular/cli": "~13.3.7",
"@angular/compiler-cli": "~13.3.0",
"@types/node": "^12.11.1",
"@types/showdown": "^2.0.0",
"@typescript-eslint/eslint-plugin": "5.17.0",
"@typescript-eslint/parser": "5.17.0",
"eslint": "^8.12.0",
"typescript": "~4.6.2"
}
}

30
src/app/app-routing.module.ts Executable file
View File

@ -0,0 +1,30 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { ArchivesComponent } from './archives/archives.component';
import { PostsComponent } from './posts/posts.component';
const routes: Routes = [
{
path: '',
component: HomeComponent
},
{
path: 'archives',
component: ArchivesComponent
},
{
path: 'posts',
component: PostsComponent
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {
}

3
src/app/app.component.html Executable file
View File

@ -0,0 +1,3 @@
<app-navigation></app-navigation>
<router-outlet></router-outlet>

0
src/app/app.component.scss Executable file
View File

37
src/app/app.component.ts Executable file
View File

@ -0,0 +1,37 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
urlParams = new URLSearchParams(window.location.search);
constructor(private router: Router) {
}
ngOnInit() {
this.navigate();
}
navigate(): void {
if (this.urlParams.get('post')) {
this.router.navigate(['/posts'], { queryParams: { post: this.urlParams.get('post') }, skipLocationChange: true });
} else {
switch (this.urlParams.get('page')) { // Keeping this style of check in case more pages get added in the future.
case 'archives':
this.router.navigate(['/archives'], { skipLocationChange: true });
break;
default:
this.router.navigate(['/'], { skipLocationChange: true });
break;
}
}
}
}

31
src/app/app.module.ts Executable file
View File

@ -0,0 +1,31 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';
import { NavigationComponent } from '@shared/components/navigation/navigation.component';
import { HomeComponent } from './home/home.component';
import { PostsComponent } from './posts/posts.component';
import { ArchivesComponent } from './archives/archives.component';
@NgModule({
declarations: [
AppComponent,
NavigationComponent,
HomeComponent,
PostsComponent,
ArchivesComponent
],
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

View File

@ -0,0 +1,27 @@
<div class="placard">
<div class="placard-title">Archives</div>
<div class="placard-text">Here you can find all the blog posts.</div>
</div>
<div class="text-center">
<input type="text" placeholder="Search by title" (keyup)="searchArchive($event.target!.value)">
&nbsp;
<button class="btn-action" type="button" (click)="searchArchive('', true);">Clear</button>
</div>
<div class="list-group" *ngIf="filteredArchives.length > 0">
<ng-container *ngFor="let archive of filteredArchives">
<a href="./?post={{archive.filename}}">
<div class="list-item mouse-hover title">
{{archive.postTitle}}
<div class="timestamp" *ngIf="!!archive.timestamp">{{parseTimestamp(archive.timestamp)}}</div>
</div>
</a>
</ng-container>
</div>
<div class="margin-y text-center" *ngIf="!!settings.username">
<a href="./">
<button type="button" class="margins-sides btn-secondary btn-fat">.HOME.</button>
</a>
</div>

View File

@ -0,0 +1,9 @@
input[type="text"] {
width: 30%;
}
@media only screen and (hover: none) {
input[type="text"] {
width: 55%;
}
}

View File

@ -0,0 +1,57 @@
import { Component, OnInit } from '@angular/core';
import { IArchive } from '@shared/models/archive.model';
import { ISettings } from '@shared/models/settings.model';
import { HelperService } from '@shared/services/helper.service';
@Component({
selector: 'app-archives',
templateUrl: './archives.component.html',
styleUrls: ['./archives.component.scss']
})
export class ArchivesComponent implements OnInit {
urlParams = new URLSearchParams(window.location.search);
archives: IArchive[] = [];
filteredArchives: IArchive[] = [];
settings: ISettings = {} as ISettings;
constructor(private helperService: HelperService) { }
ngOnInit(): void {
this.loadSettings();
}
loadSettings(): void {
this.helperService.getSettings().subscribe({
next: (data: ISettings) => {
this.settings = data;
},
complete: () => {
this.loadArchive();
}
});
}
loadArchive(): void {
this.helperService.getArchive().subscribe(data => {
this.archives = data;
this.filteredArchives = data;
});
}
parseTimestamp(timestamp: string): string {
return !!timestamp ? '(' + this.helperService.parseTimestamp(timestamp) + ')' : '';
}
searchArchive(arg: string, clear?: boolean): void {
if (!!arg) {
this.filteredArchives = this.archives.filter(entry => entry.postTitle.toLowerCase().includes(arg.toLowerCase()));
} else {
this.filteredArchives = this.archives;
}
if (clear) {
(document.getElementById('search') as HTMLInputElement).value = '';
}
}
}

View File

@ -0,0 +1,20 @@
<div class="card margins" *ngFor="let content of contents">
<div class="card-header">
<a class="title" href="./?post={{content.filename}}">{{content.postTitle}}</a>
<div class="timestamp" *ngIf="!!content.timestamp">
<strong>Posted on</strong>&nbsp;{{helperService.parseTimestamp(content.timestamp)}}
</div>
<div class="timestamp" *ngIf="!!content.editedTimestamp">
<strong>Edited on</strong>&nbsp;{{helperService.parseTimestamp(content.editedTimestamp)}}
</div>
</div>
<div class="card-body">
<p class="card-text" [innerHTML]="generateHTML(content.postContent)"></p>
</div>
</div>
<div class="text-center">
<a href="./?page=archives">
<button type="button" class="btn-fat btn-primary margins-sides margin-y">.ARCHIVES.</button>
</a>
</div>

View File

82
src/app/home/home.component.ts Executable file
View File

@ -0,0 +1,82 @@
import { Component, OnInit } from '@angular/core';
import { IArchive } from '@shared/models/archive.model';
import { PostModel } from '@shared/models/post.model';
import { ISettings } from '@shared/models/settings.model';
import { HelperService } from '@shared/services/helper.service';
import * as showdown from 'showdown';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {
contents: PostModel[] = [];
archives: IArchive[] = [];
settings: ISettings = {} as ISettings;
postsLoaded = 0;
showdownmd: any;
constructor(public helperService: HelperService) {
}
ngOnInit(): void {
this.showdownmd = new showdown.Converter();
this.helperService.getSettings().subscribe({
next: (data: ISettings) => {
this.settings = data;
},
complete: () => {
this.loadArchive();
}
});
}
loadArchive(): void {
this.helperService.getArchive().subscribe({
next: (data: IArchive[]) => {
this.archives = data;
this.settings.maxPosts = this.archives.length < this.settings.maxPosts ? this.archives.length : this.settings.maxPosts;
},
complete: () => {
this.loadPosts();
}
});
}
loadPosts(): void {
if (this.postsLoaded < this.settings.maxPosts) {
this.helperService.getPost('./assets/posts/' + this.archives[this.postsLoaded].filename + '.json').subscribe({
next: (data: PostModel) => {
this.contents.push(data);
},
complete: () => {
this.continue();
},
error: error => {
this.continue(error);
}
});
}
}
continue(error?: any): void {
if (error) {
const errorPost = new PostModel();
errorPost.postTitle = 'Aw shucks!';
errorPost.postContent = 'We couldn\'t load this post! Sorry! <strong>(' + error.status + ' ' + error.statusText + ')</strong>';
errorPost.filename = 'not-found';
this.contents.push(errorPost);
}
this.postsLoaded += 1;
this.loadPosts();
}
generateHTML(markdown: string): void {
return this.showdownmd.makeHtml(markdown);
}
}

View File

@ -0,0 +1,57 @@
<div class="card margins">
<h4 class="card-header">
<a class="title" href="./?post={{content.filename}}">{{content.postTitle}}</a>
<div class="timestamp" *ngIf="content.draft">
<strong>Draft: Post not published.</strong>
</div>
<div class="timestamp" *ngIf="!!content.timestamp">
<strong>Posted on</strong>&nbsp;{{content.timestamp}}
</div>
<div class="timestamp" *ngIf="!!content.editedTimestamp">
<strong>Edited on</strong>&nbsp;{{content.editedTimestamp}}
</div>
</h4>
<div class="card-body">
<p class="card-text" [innerHTML]="generateHTML(content.postContent)"></p>
</div>
</div>
<div class="profile-box" *ngIf="!!settings.username">
<div class="avatar">
<img src="./assets/{{settings.avatar}}" *ngIf="!!settings.avatar"><br>
</div>
<div class="bio-container">
<div class="bio-username">{{settings.username}}</div>
<div class="bio-text">{{settings.bio}}</div>
<div class="social-networks">
<a *ngIf="!!settings.twitter" href="{{settings.twitter}}" target="_blank">
<img src="./assets/social/twitter.png">
</a>
<a *ngIf="!!settings.mastodon" href="{{settings.mastodon}}" target="_blank">
<img src="./assets/social/mastodon.png">
</a>
<a *ngIf="!!settings.instagram" href="{{settings.instagram}}" target="_blank">
<img src="./assets/social/instagram.png">
</a>
<a *ngIf="!!settings.youtube" href="{{settings.youtube}}" target="_blank">
<img src="./assets/social/youtube.png">
</a>
</div>
</div>
</div>
<div class="margin-y text-center" *ngIf="settings.username">
<a href="./">
<button type="button" class="margins-sides btn-secondary btn-fat">.HOME.</button>
</a>
<br />
<a href="./?page=archives">
<button type="button" class="margin-top margins-sides btn-primary btn-fat">
.ARCHIVES.
</button>
</a>
</div>

View File

@ -0,0 +1,56 @@
.profile-box {
display: flex !important;
padding: 1rem;
margin-top: 5rem !important;
margin: 0 auto;
background-color: rgb(56, 56, 56);
box-shadow: #000a 4px 4px;
width: 50%;
.avatar {
padding: 0 1rem 0 1rem;
display: inline-block;
max-width: 200px;
max-height: 150px;
}
.avatar img {
border: 1px solid black;
max-width: 200px;
max-height: 150px;
display: block;
}
.bio-container {
padding-left: 1rem;
.bio-username {
color: rgb(255, 255, 255);
text-shadow: 0px 0px 10px #00bc97;
font-size: larger;
}
.bio-text {
color: #bbb;
}
.social-networks {
padding-top: 1rem;
}
}
}
@media only screen and (hover: none) {
.margins {
margin: unset;
}
.profile-box {
width: 90%;
margin: 0 auto;
.avatar {
padding: 0;
}
}
}

View File

@ -0,0 +1,57 @@
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { PostModel } from '@shared/models/post.model';
import { ISettings } from '@shared/models/settings.model';
import { HelperService } from '@shared/services/helper.service';
import * as showdown from 'showdown';
@Component({
selector: 'app-posts',
templateUrl: './posts.component.html',
styleUrls: ['./posts.component.scss']
})
export class PostsComponent implements OnInit {
urlParams = new URLSearchParams(window.location.search);
content: PostModel = new PostModel();
settings: ISettings = {} as ISettings;
showdownmd: any;
constructor(private titleService: Title, private helperService: HelperService) {
}
ngOnInit(): void {
this.showdownmd = new showdown.Converter();
this.helperService.getSettings().subscribe({
next: (data: ISettings) => {
this.settings = data;
},
complete: () => {
if (this.urlParams.get('post')) {
this.loadPost();
}
}
});
}
loadPost(): void {
this.helperService.getPost('./assets/posts/' + this.urlParams.get('post') + '.json').subscribe({
next: (data: PostModel) => {
this.content = data;
this.content.timestamp = !!this.content.timestamp ? this.helperService.parseTimestamp(data.timestamp) : '';
this.content.editedTimestamp = !!this.content.editedTimestamp ? this.helperService.parseTimestamp(data.editedTimestamp) : '';
this.titleService.setTitle(this.content.postTitle + ' ~ ' + this.settings.blogTitle);
},
error: (error) => {
this.content.postTitle = 'Whoops!';
this.content.postContent = 'We couldn\'t load this post! <strong>(' + error.status + ')</strong>';
}
});
}
generateHTML(markdown: string): void {
return this.showdownmd.makeHtml(markdown);
}
}

0
src/assets/.gitkeep Executable file
View File

BIN
src/assets/avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

7
src/assets/posts/archive.json Executable file
View File

@ -0,0 +1,7 @@
[
{
"postTitle": "Hello World!",
"timestamp": "1654062776",
"filename": "hello-world"
}
]

View File

@ -0,0 +1,8 @@
{
"postTitle": "Hello World!",
"timestamp": "1654062776",
"editedTimestamp": "",
"postContent": "Hello World!",
"filename": "hello-world",
"draft": false
}

15
src/assets/settings.json Executable file
View File

@ -0,0 +1,15 @@
{
"blogTitle": "Your Blog",
"avatar": "avatar.png",
"username": "Username",
"bio": "Lorem ipsum...",
"twitter": "",
"mastodon": "",
"instagram": "",
"youtube": "",
"maxPosts": 5,
"customUrl": {
"title": "GitHub",
"url": "https://github.com/"
}
}

BIN
src/assets/social/facebook.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 B

BIN
src/assets/social/instagram.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
src/assets/social/linkedin.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 B

BIN
src/assets/social/mastodon.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
src/assets/social/twitter.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 956 B

BIN
src/assets/social/youtube.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,3 @@
export const environment = {
production: true,
};

16
src/environments/environment.ts Executable file
View File

@ -0,0 +1,16 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false,
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.

BIN
src/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

15
src/index.html Executable file
View File

@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>...</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<noscript>Hey, any chance you could turn JS on? This blog runs on Angular. But you ain't missing anything so... Your choice. Cheers!</noscript>
<app-root></app-root>
</body>
</html>
<!-- There is fuck. -->

12
src/main.ts Executable file
View File

@ -0,0 +1,12 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));

53
src/polyfills.ts Executable file
View File

@ -0,0 +1,53 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes recent versions of Safari, Chrome (including
* Opera), Edge on the desktop, and iOS and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
* because those flags need to be set before `zone.js` being loaded, and webpack
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags';
*
* The flags allowed in zone-flags.ts are listed here.
*
* The following flags will work for all browsers.
*
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*
* (window as any).__Zone_enable_cross_context_check = true;
*
*/
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/

View File

@ -0,0 +1,13 @@
<div class="topnav" *ngIf="!!settings.username">
<a class="no-focus" href="./">{{settings.blogTitle}}</a>
<a href="./" [class.active]="isCurrentPage('/')">Home</a>
<a href="./?page=archives" [class.active]="isCurrentPage('/archives')">Archives</a>
<a *ngIf="!!settings?.customUrl?.url" href="{{settings?.customUrl?.url}}" target="_blank">{{settings?.customUrl?.title}}</a>
<a href="javascript:void(0);" class="icon" (click)="openMenu()">
&#xFE0E;
</a>
<a class="right-align no-focus" href="http://github.com/pedroCX486/ngx-dumblog" target="_blank" rel="noopener">
Powered by<br />ngx-dumblog
</a>
</div>

View File

@ -0,0 +1,76 @@
.topnav {
min-height: 48px;
overflow: hidden;
background-color: #333;
position: relative;
top: 0;
left: 0;
right: 0;
z-index: 2;
box-shadow: #000a 4px 4px;
}
.topnav a {
float: left;
display: block;
color: #f2f2f2;
text-align: center;
padding: 15px 16px;
text-decoration: none;
font-size: 17px;
border-right: rgb(159, 159, 159) 1px solid;
}
.topnav a:last-child {
border-right: unset;
}
.topnav a:hover:not(.no-focus) {
background-color: #ddd;
color: black;
}
.topnav a.active {
background-color: rgb(97, 0, 162);
color: white;
}
.topnav .icon {
display: none;
}
@media screen and (max-width: 600px) {
.topnav a:not(:first-child) {
display: none;
}
.topnav a.icon {
float: right;
display: block;
}
.topnav.responsive {
position: relative;
top: 0;
left: 0;
right: 0;
}
.topnav.responsive a {
float: none;
display: block;
text-align: left;
}
.topnav.responsive .icon {
position: absolute;
right: 0;
top: 0;
}
}
.right-align {
float: right !important;
font-size: 8px !important;
font-family: 'pc98';
}

View File

@ -0,0 +1,48 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { ISettings } from '@shared/models/settings.model';
import { HelperService } from '@shared/services/helper.service';
import { Title } from '@angular/platform-browser';
@Component({
selector: 'app-navigation',
templateUrl: './navigation.component.html',
styleUrls: ['./navigation.component.scss']
})
export class NavigationComponent implements OnInit {
settings: ISettings = {} as ISettings;
constructor(private router: Router, private helperService: HelperService, private titleService: Title) {
}
ngOnInit(): void {
this.initialize();
}
initialize(): void {
this.helperService.getSettings().subscribe({
next: (data: ISettings) => {
this.settings = data;
},
complete: () => {
this.titleService.setTitle(this.settings.blogTitle);
}
});
}
openMenu(): void {
const element = document.getElementsByClassName('topnav')[0];
if (element.className === 'topnav') {
element.className += ' responsive';
} else {
element.className = 'topnav';
}
}
isCurrentPage(url: string): boolean {
return this.router.url === url;
}
}

View File

@ -0,0 +1,5 @@
export interface IArchive {
postTitle: string;
timestamp: string;
filename: string;
}

View File

@ -0,0 +1,8 @@
export class PostModel {
postTitle = '';
timestamp = '';
editedTimestamp = '';
postContent = '';
filename = '';
draft = false;
}

View File

@ -0,0 +1,17 @@
export interface ISettings {
blogTitle: string;
avatar: string;
username: string;
bio: string;
twitter: string;
mastodon: string;
instagram: string;
youtube: string;
maxPosts: number;
customUrl: ICustomUrl;
}
export interface ICustomUrl {
title: string;
url: string;
}

View File

@ -0,0 +1,28 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class HelperService {
constructor(private httpClient: HttpClient) { }
getSettings(): Observable<any> {
return this.httpClient.get('./assets/settings.json?latest=' + this.generateTimestamp());
}
getArchive(): Observable<any> {
return this.httpClient.get('./assets/posts/archive.json?latest=' + this.generateTimestamp());
}
getPost(url: string): Observable<any> {
return this.httpClient.get(url + '?latest='+ this.generateTimestamp());
}
generateTimestamp(): string {
return Math.round((new Date()).getTime() / 1000).toString();
}
parseTimestamp(timestamp: string): string {
return new Date(Number(timestamp) * 1000).toUTCString();
}
}

59
src/shared/styles/buttons.scss Executable file
View File

@ -0,0 +1,59 @@
// Buttons
button {
cursor: pointer;
transition: all 0.3s ease-in-out;
text-decoration: none;
padding: 0.6rem;
border: none;
box-shadow: #000a 4px 4px;
font-family: 'pxplus_ibm_vga8regular';
}
// Misc.
.btn-fat {
padding: 0.7rem !important;
}
// Primary
.btn-primary {
background-color: rgb(97, 0, 162);
color: white;
font-size: 1rem;
}
.btn-primary:hover {
box-shadow: 0px 0px 7px rgb(97, 0, 162);
}
// Secondary
.btn-secondary {
background-color: #00d199;
color: #444;
font-size: 1rem;
}
.btn-secondary:hover {
box-shadow: 0px 0px 7px #00d199;
}
// Tertiary
.btn-tertiary {
background-color: #17a2b8;
color: white;
font-size: 1rem;
}
.btn-tertiary:hover {
box-shadow: 0px 0px 7px #17a2b8;
}
// Action
.btn-action {
background-color: #00905e;
color: white;
font-size: 1rem;
}
.btn-action:hover {
box-shadow: 0px 0px 7px #00905e;
}

36
src/shared/styles/cards.scss Executable file
View File

@ -0,0 +1,36 @@
// Cards
.card {
border: none;
box-shadow: #000a 4px 4px;
}
.card-header {
border-bottom: none;
background-color: rgb(97, 0, 162);
padding: 1rem;
margin-bottom: 0;
}
.title {
color: #00ffea !important;
text-decoration: none;
font-size: 1.5rem;
font-weight: 500;
font-family: 'pc98';
}
.timestamp {
color: #b5b5b5;
font-size: 12px;
font-family: 'pc98';
}
.card-body {
padding: 1rem;
background-color: rgb(46, 46, 46);
margin-top: -10px;
color: #bbb;
}
// .card-text {
// }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

17
src/shared/styles/inputs.scss Executable file
View File

@ -0,0 +1,17 @@
// Inputs
::placeholder {
color: rgb(208, 208, 208);
opacity: 1;
}
input[type="text"] {
padding: 10px;
border: 1px solid rgb(138, 138, 138);
resize: vertical;
min-width: 5%;
background-color: transparent;
color: white;
font-size: 15px;
box-shadow: #000a 4px 4px;
font-family: 'pxplus_ibm_vga8regular';
}

17
src/shared/styles/lists.scss Executable file
View File

@ -0,0 +1,17 @@
// Lists
.list-group {
padding: 2rem;
}
.list-item {
padding: 1rem;
border: rgb(88, 88, 88) solid 1px;
background-color: rgb(46, 46, 46);
margin-top: 0.5rem;
box-shadow: #000a 4px 4px;
}
.list-item:hover {
background-color: rgb(52, 39, 62);
cursor: pointer;
}

16
src/shared/styles/placard.scss Executable file
View File

@ -0,0 +1,16 @@
// Placard
.placard {
background-color: rgb(97, 0, 162);
text-align: center;
padding: 2rem;
margin: 2rem;
font-weight: 200;
font-size: 1.25rem;
color: white;
box-shadow: #000a 4px 4px;
}
.placard-title {
font-weight: 200;
font-size: 3.5rem;
}

23
src/shared/styles/utils.scss Executable file
View File

@ -0,0 +1,23 @@
//Margins
.margins {
margin: 1rem;
}
.margins-sides {
width: 40%;
margin: 0 auto;
}
.margin-top {
margin-top: 0.5rem !important;
}
.margin-y {
margin-top: 2rem;
margin-bottom: 3rem;
}
// Alignments
.text-center {
text-align: center;
}

42
src/styles.scss Executable file
View File

@ -0,0 +1,42 @@
// Imports
@import "./shared/styles/buttons.scss";
@import "./shared/styles/cards.scss";
@import "./shared/styles/inputs.scss";
@import "./shared/styles/lists.scss";
@import "./shared/styles/placard.scss";
@import "./shared/styles/utils.scss";
// Global
@font-face {
font-family: "pxplus_ibm_vga8regular";
src: url("./shared/styles/fonts/PxPlus_IBM_VGA8.woff") format("woff"),
url("./shared/styles/fonts//PxPlus_IBM_VGA8.ttf") format("truetype");
font-weight: normal;
}
@font-face {
font-family: "pc98";
src: url("./shared/styles/fonts/PC_9800.woff2") format("woff2"),
url("./shared/styles/fonts/PC_9800.woff") format("woff"),
url("./shared/styles/fonts/pc-9800.ttf") format("truetype");
font-weight: normal;
font-style: normal;
}
// Body
body {
line-height: 1.2;
font-family: "pxplus_ibm_vga8regular";
margin-left: 0.5em;
margin-right: 0.5em;
background-color: #444;
}
// Anchors
a {
color: #00ffea;
text-decoration: none;
}
a:visited {
color: #00905e;
text-decoration: none;
}

15
tsconfig.app.json Executable file
View File

@ -0,0 +1,15 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": [
"src/main.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.d.ts"
]
}

40
tsconfig.json Executable file
View File

@ -0,0 +1,40 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@shared/*": [
"src/shared/*"
],
"@env/*": [
"src/environments/*"
]
},
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "es2017",
"module": "es2020",
"lib": [
"es2020",
"dom"
]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": false
}
}