├── .editorconfig
├── .gitignore
├── .vscode
├── extensions.json
├── launch.json
└── tasks.json
├── README.md
├── angular.json
├── favicon.ico
├── index.html
├── package-lock.json
├── package.json
├── src
├── app
│ ├── app.component.spec.ts
│ ├── app.component.ts
│ ├── app.config.server.ts
│ ├── app.config.ts
│ ├── components
│ │ ├── comment.component.ts
│ │ ├── nav.component.ts
│ │ ├── stories.component.ts
│ │ └── toggle.component.ts
│ ├── hn.service.ts
│ ├── models.ts
│ └── pages
│ │ ├── [...stories].page.ts
│ │ ├── stories
│ │ └── [id].page.ts
│ │ └── users
│ │ └── [id].page.ts
├── assets
│ ├── .gitkeep
│ ├── analog.svg
│ └── vite.svg
├── main.server.ts
├── main.ts
├── server
│ └── routes
│ │ └── v1
│ │ └── hello.ts
├── styles.css
├── test.ts
└── vite-env.d.ts
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.spec.json
└── vite.config.ts
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see https://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.ts]
12 | quote_type = single
13 |
14 | [*.md]
15 | max_line_length = off
16 | trim_trailing_whitespace = false
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # Compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 | /bazel-out
8 |
9 | # Node
10 | /node_modules
11 | npm-debug.log
12 | yarn-error.log
13 |
14 | # IDEs and editors
15 | .idea/
16 | .project
17 | .classpath
18 | .c9/
19 | *.launch
20 | .settings/
21 | *.sublime-workspace
22 |
23 | # Visual Studio Code
24 | .vscode/*
25 | !.vscode/settings.json
26 | !.vscode/tasks.json
27 | !.vscode/launch.json
28 | !.vscode/extensions.json
29 | .history/*
30 |
31 | # Miscellaneous
32 | /.angular/cache
33 | .sass-cache/
34 | /connect.lock
35 | /coverage
36 | /libpeerconnection.log
37 | testem.log
38 | /typings
39 |
40 | # System files
41 | .DS_Store
42 | Thumbs.db
43 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
3 | "recommendations": ["angular.ng-template"]
4 | }
5 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
3 | "version": "0.2.0",
4 | "configurations": [
5 | {
6 | "name": "ng serve",
7 | "type": "chrome",
8 | "request": "launch",
9 | "preLaunchTask": "npm: start",
10 | "url": "http://localhost:5173/"
11 | },
12 | {
13 | "name": "ng test",
14 | "type": "chrome",
15 | "request": "launch",
16 | "preLaunchTask": "npm: test"
17 | }
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
3 | "version": "2.0.0",
4 | "tasks": [
5 | {
6 | "type": "npm",
7 | "script": "start",
8 | "isBackground": true,
9 | "problemMatcher": {
10 | "owner": "typescript",
11 | "pattern": "$tsc",
12 | "background": {
13 | "activeOnStart": true,
14 | "beginsPattern": {
15 | "regexp": "(.*?)"
16 | },
17 | "endsPattern": {
18 | "regexp": "bundle generation complete"
19 | }
20 | }
21 | }
22 | },
23 | {
24 | "type": "npm",
25 | "script": "test",
26 | "isBackground": true,
27 | "problemMatcher": {
28 | "owner": "typescript",
29 | "pattern": "$tsc",
30 | "background": {
31 | "activeOnStart": true,
32 | "beginsPattern": {
33 | "regexp": "(.*?)"
34 | },
35 | "endsPattern": {
36 | "regexp": "bundle generation complete"
37 | }
38 | }
39 | }
40 | }
41 | ]
42 | }
43 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Analog App
2 |
3 | This project was generated with [Analog](https://analogjs.org), the fullstack meta-framework for Angular.
4 |
5 | ## Setup
6 |
7 | Run `npm install` to install the application dependencies.
8 |
9 | ## Development
10 |
11 | Run `npm start` for a dev server. Navigate to `http://localhost:5173/`. The application automatically reloads if you change any of the source files.
12 |
13 | ## Build
14 |
15 | Run `npm run build` to build the client/server project. The client build artifacts are located in the `dist/analog/public` directory. The server for the API build artifacts are located in the `dist/analog/server` directory.
16 |
17 | ## Test
18 |
19 | Run `npm run test` to run unit tests with [Vitest](https://vitest.dev).
20 |
21 | ## Community
22 |
23 | - Visit and Star the [GitHub Repo](https://github.com/analogjs/analog)
24 | - Join the [Discord](https://chat.analogjs.org)
25 | - Follow us on [Twitter](https://twitter.com/analogjs)
26 | - Become a [Sponsor](https://github.com/sponsors/brandonroberts)
27 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "my-app": {
7 | "projectType": "application",
8 | "root": "",
9 | "sourceRoot": "src",
10 | "prefix": "app",
11 | "architect": {
12 | "build": {
13 | "builder": "@nx/vite:build",
14 | "options": {
15 | "configFile": "vite.config.ts",
16 | "main": "src/main.ts",
17 | "outputPath": "dist/client"
18 | },
19 | "defaultConfiguration": "production",
20 | "configurations": {
21 | "development": {
22 | "mode": "development"
23 | },
24 | "production": {
25 | "sourcemap": false,
26 | "mode": "production"
27 | }
28 | }
29 | },
30 | "serve": {
31 | "builder": "@nx/vite:dev-server",
32 | "defaultConfiguration": "development",
33 | "options": {
34 | "buildTarget": "my-app:build",
35 | "port": 5173
36 | },
37 | "configurations": {
38 | "development": {
39 | "buildTarget": "my-app:build:development",
40 | "hmr": true
41 | },
42 | "production": {
43 | "buildTarget": "my-app:build:production"
44 | }
45 | }
46 | },
47 | "test": {
48 | "builder": "@nx/vite:test"
49 | }
50 | }
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brandonroberts/analog-hackernews/d6d5659f8336e9c5126e14e22c467a06d6d93a04/favicon.ico
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Analog Hacker News
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "analog-hackernews",
3 | "version": "0.0.0",
4 | "private": true,
5 | "engines": {
6 | "node": "^16.14.0 || >=18.10.0"
7 | },
8 | "scripts": {
9 | "dev": "ng serve",
10 | "ng": "ng",
11 | "start": "npm run dev",
12 | "build": "ng build",
13 | "watch": "ng build --watch",
14 | "test": "ng test"
15 | },
16 | "dependencies": {
17 | "@analogjs/content": "^0.2.0-beta.25",
18 | "@analogjs/router": "^0.2.0-beta.25",
19 | "@angular/animations": "^16.1.0",
20 | "@angular/common": "^16.1.0",
21 | "@angular/compiler": "^16.1.0",
22 | "@angular/core": "^16.1.0",
23 | "@angular/forms": "^16.1.0",
24 | "@angular/platform-browser": "^16.1.0",
25 | "@angular/platform-browser-dynamic": "^16.1.0",
26 | "@angular/platform-server": "^16.1.0",
27 | "@angular/router": "^16.1.0",
28 | "@nx/angular": "^16.4.0",
29 | "front-matter": "^4.0.2",
30 | "marked": "^5.0.2",
31 | "marked-gfm-heading-id": "^3.0.4",
32 | "marked-highlight": "^2.0.1",
33 | "prismjs": "^1.29.0",
34 | "rxjs": "~7.5.6",
35 | "tslib": "^2.4.0",
36 | "zone.js": "~0.13.0"
37 | },
38 | "devDependencies": {
39 | "@analogjs/platform": "^0.2.0-beta.25",
40 | "@angular-devkit/build-angular": "^16.1.0",
41 | "@angular/cli": "^16.1.0",
42 | "@angular/compiler-cli": "^16.1.0",
43 | "@nx/vite": "^16.4.0",
44 | "nx": "^16.4.0",
45 | "jsdom": "^22.1.0",
46 | "typescript": "~5.0.2",
47 | "vite": "^4.3.9",
48 | "vitest": "^0.32.0"
49 | }
50 | }
--------------------------------------------------------------------------------
/src/app/app.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 | import { RouterTestingModule } from '@angular/router/testing';
3 | import { AppComponent } from './app.component';
4 |
5 | describe('AppComponent', () => {
6 | beforeEach(async () => {
7 | await TestBed.configureTestingModule({
8 | imports: [RouterTestingModule, AppComponent],
9 | }).compileComponents();
10 | });
11 |
12 | it('should create the app', () => {
13 | const fixture = TestBed.createComponent(AppComponent);
14 | const app = fixture.componentInstance;
15 | expect(app).toBeTruthy();
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { RouterOutlet } from '@angular/router';
3 |
4 | import { NavComponent } from './components/nav.component';
5 |
6 | @Component({
7 | selector: 'app-root',
8 | standalone: true,
9 | imports: [NavComponent, RouterOutlet],
10 | template: `
11 |
12 |
13 | `
14 | })
15 | export class AppComponent {}
16 |
--------------------------------------------------------------------------------
/src/app/app.config.server.ts:
--------------------------------------------------------------------------------
1 | import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
2 | import {
3 | provideServerRendering,
4 | ɵSERVER_CONTEXT as SERVER_CONTEXT,
5 | } from '@angular/platform-server';
6 | import { appConfig } from './app.config';
7 |
8 | const serverConfig: ApplicationConfig = {
9 | providers: [
10 | provideServerRendering(),
11 | { provide: SERVER_CONTEXT, useValue: 'ssr-analog' },
12 | ],
13 | };
14 |
15 | export const config = mergeApplicationConfig(appConfig, serverConfig);
16 |
--------------------------------------------------------------------------------
/src/app/app.config.ts:
--------------------------------------------------------------------------------
1 | import { provideHttpClient } from '@angular/common/http';
2 | import { ApplicationConfig } from '@angular/core';
3 | import { provideClientHydration } from '@angular/platform-browser';
4 | import { provideFileRouter } from '@analogjs/router';
5 | import { withComponentInputBinding } from '@angular/router';
6 |
7 | export const appConfig: ApplicationConfig = {
8 | providers: [
9 | provideFileRouter(withComponentInputBinding()),
10 | provideHttpClient(),
11 | provideClientHydration(),
12 | ],
13 | };
14 |
--------------------------------------------------------------------------------
/src/app/components/comment.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, forwardRef } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 | import { RouterLink } from '@angular/router';
4 |
5 | import { Comment } from '../models';
6 | import { ToggleComponent } from './toggle.component';
7 |
8 | @Component({
9 | selector: 'app-comment',
10 | standalone: true,
11 | imports: [RouterLink, CommonModule, forwardRef(() => CommentComponent), ToggleComponent],
12 | template: `
13 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | `,
26 | styles: [
27 | ]
28 | })
29 | export class CommentComponent {
30 | @Input() comment!: Comment;
31 | }
32 |
--------------------------------------------------------------------------------
/src/app/components/nav.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from "@angular/core";
2 | import { RouterLink, RouterLinkActive } from "@angular/router";
3 |
4 | @Component({
5 | selector: 'app-nav',
6 | standalone: true,
7 | imports: [RouterLink, RouterLinkActive],
8 | template: `
9 |
36 | `
37 | })
38 | export class NavComponent {}
--------------------------------------------------------------------------------
/src/app/components/stories.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from "@angular/core";
2 | import { NgIf, NgFor } from "@angular/common";
3 | import { RouterLink } from "@angular/router";
4 |
5 | import { Story } from "../models";
6 |
7 | @Component({
8 | selector: 'app-stories',
9 | standalone: true,
10 | imports: [NgIf, NgFor, RouterLink],
11 | template: `
12 |
39 | `
40 | })
41 | export class StoriesComponent {
42 | @Input() stories: Story[] = [];
43 | }
--------------------------------------------------------------------------------
/src/app/components/toggle.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, signal } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 |
4 | @Component({
5 | selector: 'app-toggle',
6 | standalone: true,
7 | imports: [CommonModule],
8 | template: `
9 |
14 |
17 | `,
18 | styles: [
19 | ]
20 | })
21 | export class ToggleComponent {
22 | open = signal(true);
23 |
24 | toggle(state: boolean) {
25 | this.open.set(!state);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/hn.service.ts:
--------------------------------------------------------------------------------
1 | import { HttpClient } from "@angular/common/http";
2 | import { Injectable, inject } from "@angular/core";
3 | import { catchError, of } from "rxjs";
4 |
5 | import { Story, User } from "./models";
6 |
7 | @Injectable({
8 | providedIn: 'root'
9 | })
10 | export class HNService {
11 | private http = inject(HttpClient);
12 |
13 | getStory(id: string) {
14 | return this.http
15 | .get(`https://node-hnapi.herokuapp.com/item/${id}`)
16 | .pipe(catchError(() => of(null)));
17 | }
18 |
19 | getStories(path: string) {
20 | return this.http
21 | .get(`https://node-hnapi.herokuapp.com/${path}`)
22 | .pipe(catchError(() => of([])));
23 | }
24 |
25 | getUser(user: string) {
26 | return this.http
27 | .get(`https://hacker-news.firebaseio.com/v0/user/${user}.json`)
28 | .pipe(catchError(() => of(null)));
29 | }
30 | }
--------------------------------------------------------------------------------
/src/app/models.ts:
--------------------------------------------------------------------------------
1 | export interface Comment {
2 | user: string;
3 | time_ago: string;
4 | content: string;
5 | comments: Comment[];
6 | }
7 |
8 | export interface Story {
9 | id: string;
10 | points: string;
11 | url: string;
12 | title: string;
13 | domain: string;
14 | type: string;
15 | time_ago: string;
16 | user: string;
17 | comments_count: number;
18 | comments: Comment[];
19 | }
20 |
21 | export interface User {
22 | error: string;
23 | id: string;
24 | created: string;
25 | karma: number;
26 | about: string;
27 | }
--------------------------------------------------------------------------------
/src/app/pages/[...stories].page.ts:
--------------------------------------------------------------------------------
1 | import { Component, DestroyRef, inject, signal } from '@angular/core';
2 | import { NavigationEnd, Router } from '@angular/router';
3 | import { filter, map, merge, of, startWith, switchMap, tap } from 'rxjs';
4 | import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
5 |
6 | import { StoriesComponent } from '../components/stories.component';
7 | import { HNService } from '../hn.service';
8 | import { Story } from '../models';
9 |
10 | const mapStories: Record = {
11 | top: "news",
12 | new: "newest",
13 | show: "show",
14 | ask: "ask",
15 | job: "jobs",
16 | };
17 |
18 |
19 | @Component({
20 | selector: 'app-home',
21 | standalone: true,
22 | imports: [StoriesComponent],
23 | template: `
24 |
29 | `
30 | })
31 | export default class StoriesPageComponent {
32 | stories = signal([]);
33 |
34 | hnService = inject(HNService);
35 | router = inject(Router);
36 | destroyRef = inject(DestroyRef);
37 |
38 | ngOnInit() {
39 | merge(
40 | this.router.events.pipe(
41 | startWith(this.router.url),
42 | filter(e => e instanceof NavigationEnd),
43 | map(() => this.router.url)),
44 | of(this.router.url)
45 | ).pipe(
46 | switchMap(url => this.hnService.getStories(mapStories[url.substring(1)] || 'newest')),
47 | takeUntilDestroyed(this.destroyRef),
48 | tap(stories => this.stories.set(stories))
49 | ).subscribe();
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/app/pages/stories/[id].page.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, inject, signal } from "@angular/core";
2 | import { NgFor, NgIf } from "@angular/common";
3 | import { RouterLink } from "@angular/router";
4 |
5 | import { HNService } from "../../hn.service";
6 | import { Story } from "../../models";
7 | import { CommentComponent } from "../../components/comment.component";
8 |
9 | @Component({
10 | selector: 'app-story',
11 | standalone: true,
12 | imports: [NgIf, NgFor, RouterLink, CommentComponent],
13 | template: `
14 |
15 |
25 |
35 |
36 | `
37 | })
38 | export default class StoryPageComponent {
39 | story = signal(null);
40 | hnService = inject(HNService);
41 | @Input() id!: string;
42 |
43 | ngOnInit() {
44 | this.hnService.getStory(this.id)
45 | .subscribe(story => this.story.set(story));
46 | }
47 | }
--------------------------------------------------------------------------------
/src/app/pages/users/[id].page.ts:
--------------------------------------------------------------------------------
1 | import { DatePipe, NgIf } from "@angular/common";
2 | import { Component, Input, OnInit, inject, signal } from "@angular/core";
3 |
4 | import { HNService } from "../../hn.service";
5 | import { User } from "../../models";
6 |
7 | @Component({
8 | selector: 'app-user-page',
9 | standalone: true,
10 | imports: [NgIf, DatePipe],
11 | template: `
12 |
40 | `
41 | })
42 | export default class UserPageComponent implements OnInit {
43 | @Input() id!: string;
44 | user = signal(null);
45 | hnService = inject(HNService);
46 |
47 | ngOnInit() {
48 | this.hnService.getUser(this.id)
49 | .subscribe(user => this.user.set(user));
50 | }
51 | }
--------------------------------------------------------------------------------
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brandonroberts/analog-hackernews/d6d5659f8336e9c5126e14e22c467a06d6d93a04/src/assets/.gitkeep
--------------------------------------------------------------------------------
/src/assets/analog.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/main.server.ts:
--------------------------------------------------------------------------------
1 | import 'zone.js/node';
2 | import { enableProdMode } from '@angular/core';
3 | import { bootstrapApplication } from '@angular/platform-browser';
4 | import { renderApplication } from '@angular/platform-server';
5 |
6 | import { config } from './app/app.config.server';
7 | import { AppComponent } from './app/app.component';
8 |
9 | if (import.meta.env.PROD) {
10 | enableProdMode();
11 | }
12 |
13 | export function bootstrap() {
14 | return bootstrapApplication(AppComponent, config);
15 | }
16 |
17 | export default async function render(url: string, document: string) {
18 | const html = await renderApplication(bootstrap, {
19 | document,
20 | url,
21 | });
22 |
23 | return html;
24 | }
25 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import 'zone.js';
2 | import { bootstrapApplication } from '@angular/platform-browser';
3 |
4 | import { AppComponent } from './app/app.component';
5 | import { appConfig } from './app/app.config';
6 |
7 | bootstrapApplication(AppComponent, appConfig);
8 |
--------------------------------------------------------------------------------
/src/server/routes/v1/hello.ts:
--------------------------------------------------------------------------------
1 | import { defineEventHandler } from 'h3';
2 |
3 | export default defineEventHandler(() => ({ message: 'Hello World' }));
4 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 | :root {
3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
4 | font-size: 15px;
5 | background-color: #f2f3f5;
6 | margin: 0;
7 | padding-top: 55px;
8 | color: #34495e;
9 | overflow-y: scroll;
10 | }
11 |
12 | a {
13 | color: #34495e;
14 | text-decoration: none;
15 | }
16 |
17 | .header {
18 | background-color: red;
19 | position: fixed;
20 | z-index: 999;
21 | height: 55px;
22 | top: 0;
23 | left: 0;
24 | right: 0;
25 | }
26 |
27 | .header .inner {
28 | max-width: 800px;
29 | box-sizing: border-box;
30 | margin: 0 auto;
31 | padding: 15px 5px;
32 | }
33 |
34 | .header a {
35 | color: rgba(255, 255, 255, 0.8);
36 | line-height: 24px;
37 | transition: color 0.15s ease;
38 | display: inline-block;
39 | vertical-align: middle;
40 | font-weight: 300;
41 | letter-spacing: 0.075em;
42 | margin-right: 1.8em;
43 | }
44 |
45 | .header a:hover {
46 | color: #fff;
47 | }
48 |
49 | .header a.active {
50 | color: #fff;
51 | font-weight: 400;
52 | }
53 |
54 | .header a:nth-child(6) {
55 | margin-right: 0;
56 | }
57 |
58 | .header .github {
59 | color: #fff;
60 | font-size: 0.9em;
61 | margin: 0;
62 | float: right;
63 | }
64 |
65 | .logo {
66 | width: 24px;
67 | margin-right: 10px;
68 | display: inline-block;
69 | vertical-align: middle;
70 | }
71 |
72 | .view {
73 | max-width: 800px;
74 | margin: 0 auto;
75 | position: relative;
76 | }
77 |
78 | @media (max-width: 860px) {
79 | .header .inner {
80 | padding: 15px 30px;
81 | }
82 | }
83 |
84 | @media (max-width: 600px) {
85 | .header .inner {
86 | padding: 15px;
87 | }
88 |
89 | .header a {
90 | margin-right: 1em;
91 | }
92 |
93 | .header .github {
94 | display: none;
95 | }
96 | }
97 |
98 | .news-view {
99 | padding-top: 45px;
100 | }
101 |
102 | .news-list, .news-list-nav {
103 | background-color: #fff;
104 | border-radius: 2px;
105 | }
106 |
107 | .news-list-nav {
108 | padding: 15px 30px;
109 | position: fixed;
110 | text-align: center;
111 | top: 55px;
112 | left: 0;
113 | right: 0;
114 | z-index: 998;
115 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
116 | }
117 |
118 | .news-list-nav .page-link {
119 | margin: 0 1em;
120 | }
121 |
122 | .news-list-nav .disabled {
123 | color: #aaa;
124 | }
125 |
126 | .news-list {
127 | position: absolute;
128 | margin: 30px 0;
129 | width: 100%;
130 | }
131 |
132 | .news-list ul {
133 | list-style-type: none;
134 | padding: 0;
135 | margin: 0;
136 | }
137 |
138 | @media (max-width: 600px) {
139 | .news-list {
140 | margin: 10px 0;
141 | }
142 | }
143 |
144 | .news-item {
145 | background-color: #fff;
146 | padding: 20px 30px 20px 80px;
147 | border-bottom: 1px solid #eee;
148 | position: relative;
149 | line-height: 20px;
150 | }
151 |
152 | .news-item .score {
153 | color: red;
154 | font-size: 1.1em;
155 | font-weight: 700;
156 | position: absolute;
157 | top: 50%;
158 | left: 0;
159 | width: 80px;
160 | text-align: center;
161 | margin-top: -10px;
162 | }
163 |
164 | .news-item .host, .news-item .meta {
165 | font-size: 0.85em;
166 | color: #626262;
167 | }
168 |
169 | .news-item .host a, .news-item .meta a {
170 | color: #626262;
171 | text-decoration: underline;
172 | }
173 |
174 | .news-item .host a:hover, .news-item .meta a:hover {
175 | color: red;
176 | }
177 |
178 | .item-view-header {
179 | background-color: #fff;
180 | padding: 1.8em 2em 1em;
181 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
182 | }
183 |
184 | .item-view-header h1 {
185 | display: inline;
186 | font-size: 1.5em;
187 | margin: 0;
188 | margin-right: 0.5em;
189 | }
190 |
191 | .item-view-header .host, .item-view-header .meta, .item-view-header .meta a {
192 | color: #626262;
193 | }
194 |
195 | .item-view-header .meta a {
196 | text-decoration: underline;
197 | }
198 |
199 | .item-view-comments {
200 | background-color: #fff;
201 | margin-top: 10px;
202 | padding: 0 2em 0.5em;
203 | }
204 |
205 | .item-view-comments-header {
206 | margin: 0;
207 | font-size: 1.1em;
208 | padding: 1em 0;
209 | position: relative;
210 | }
211 |
212 | .item-view-comments-header .spinner {
213 | display: inline-block;
214 | margin: -15px 0;
215 | }
216 |
217 | .comment-children {
218 | list-style-type: none;
219 | padding: 0;
220 | margin: 0;
221 | }
222 |
223 | @media (max-width: 600px) {
224 | .item-view-header h1 {
225 | font-size: 1.25em;
226 | }
227 | }
228 |
229 | .comment-children .comment-children {
230 | margin-left: 1.5em;
231 | }
232 |
233 | .comment {
234 | border-top: 1px solid #eee;
235 | position: relative;
236 | }
237 |
238 | .comment .by, .comment .text, .comment .toggle {
239 | font-size: 0.9em;
240 | margin: 1em 0;
241 | }
242 |
243 | .comment .by {
244 | color: #626262;
245 | }
246 |
247 | .comment .by a {
248 | color: #626262;
249 | text-decoration: underline;
250 | }
251 |
252 | .comment .text {
253 | overflow-wrap: break-word;
254 | }
255 |
256 | .comment .text a:hover {
257 | color: red;
258 | }
259 |
260 | .comment .text pre {
261 | white-space: pre-wrap;
262 | }
263 |
264 | .comment .toggle {
265 | background-color: #fffbf2;
266 | padding: 0.3em 0.5em;
267 | border-radius: 4px;
268 | }
269 |
270 | .comment .toggle a {
271 | color: #626262;
272 | cursor: pointer;
273 | }
274 |
275 | .comment .toggle.open {
276 | padding: 0;
277 | background-color: transparent;
278 | margin-bottom: -0.5em;
279 | }
280 |
281 | .user-view {
282 | background-color: #fff;
283 | box-sizing: border-box;
284 | padding: 2em 3em;
285 | }
286 |
287 | .user-view h1 {
288 | margin: 0;
289 | font-size: 1.5em;
290 | }
291 |
292 | .user-view .meta {
293 | list-style-type: none;
294 | padding: 0;
295 | }
296 |
297 | .user-view .label {
298 | display: inline-block;
299 | min-width: 4em;
300 | }
301 |
302 | .user-view .about {
303 | margin: 1em 0;
304 | }
305 |
306 | .user-view .links a {
307 | text-decoration: underline;
308 | }
--------------------------------------------------------------------------------
/src/test.ts:
--------------------------------------------------------------------------------
1 | import '@analogjs/vite-plugin-angular/setup-vitest';
2 |
3 | import {
4 | BrowserDynamicTestingModule,
5 | platformBrowserDynamicTesting,
6 | } from '@angular/platform-browser-dynamic/testing';
7 | import { getTestBed } from '@angular/core/testing';
8 |
9 | getTestBed().initTestEnvironment(
10 | BrowserDynamicTestingModule,
11 | platformBrowserDynamicTesting()
12 | );
13 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "./tsconfig.json",
4 | "compilerOptions": {
5 | "composite": false,
6 | "outDir": "./out-tsc/app",
7 | "types": []
8 | },
9 | "files": ["src/main.ts", "src/main.server.ts"],
10 | "include": [
11 | "src/**/*.d.ts",
12 | "src/app/routes/**/*.ts",
13 | "src/app/pages/**/*.page.ts"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "compileOnSave": false,
4 | "compilerOptions": {
5 | "baseUrl": "./",
6 | "outDir": "./dist/out-tsc",
7 | "forceConsistentCasingInFileNames": true,
8 | "strict": true,
9 | "noImplicitOverride": true,
10 | "noPropertyAccessFromIndexSignature": true,
11 | "noImplicitReturns": true,
12 | "noFallthroughCasesInSwitch": true,
13 | "sourceMap": true,
14 | "declaration": false,
15 | "downlevelIteration": true,
16 | "experimentalDecorators": true,
17 | "moduleResolution": "node",
18 | "importHelpers": true,
19 | "target": "ES2022",
20 | "module": "ES2022",
21 | "lib": ["ES2022", "dom"],
22 | "useDefineForClassFields": false,
23 | "skipLibCheck": true
24 | },
25 | "angularCompilerOptions": {
26 | "enableI18nLegacyMessageIdFormat": false,
27 | "strictInjectionParameters": true,
28 | "strictInputAccessModifiers": true,
29 | "strictTemplates": true
30 | },
31 | "references": [
32 | { "path": "tsconfig.app.json" },
33 | { "path": "tsconfig.spec.json" }
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "./tsconfig.json",
4 | "compilerOptions": {
5 | "composite": false,
6 | "outDir": "./out-tsc/spec",
7 | "types": ["node", "vitest/globals"]
8 | },
9 | "files": ["src/test.ts"],
10 | "include": ["src/**/*.spec.ts", "src/**/*.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import { defineConfig } from 'vite';
4 | import analog from '@analogjs/platform';
5 |
6 | // https://vitejs.dev/config/
7 | export default defineConfig(({ mode }) => ({
8 | publicDir: 'src/assets',
9 | build: {
10 | target: ['es2020'],
11 | },
12 | resolve: {
13 | mainFields: ['module'],
14 | },
15 | plugins: [analog({
16 | prerender: {
17 | routes: []
18 | }
19 | })],
20 | test: {
21 | globals: true,
22 | environment: 'jsdom',
23 | setupFiles: ['src/test.ts'],
24 | include: ['**/*.spec.ts'],
25 | },
26 | define: {
27 | 'import.meta.vitest': mode !== 'production',
28 | },
29 | }));
30 |
--------------------------------------------------------------------------------
27 | {{story.comments_count ? story.comments_count + ' comments' : 'No comments yet'}} 28 |
29 |30 |-
31 |
32 |
33 |
34 |