├── .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 |
14 | {{comment.user}}{{" "}}{{comment.time_ago}} 15 |
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 |
10 | 35 |
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 |
10 | 11 | {{open() ? "[-]" : "[+] comments collapsed"}} 12 | 13 |
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 |
25 |
26 | 27 |
28 |
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 |
16 | 17 |

{{story.title}}

18 |
19 | ({{story.domain}}) 20 |

21 | {{story.points}} points | by 22 | {{story.user}} {{story.time_ago}} 23 |

24 |
25 |
26 |

27 | {{story.comments_count ? story.comments_count + ' comments' : 'No comments yet'}} 28 |

29 |
    30 |
  • 31 | 32 |
  • 33 |
34 |
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 |
13 |
14 |

User : {{user.id}}

15 |
    16 |
  • 17 | Created: {{user.created}} 18 |
  • 19 |
  • 20 | Karma: {{user.karma}} 21 |
  • 22 |
  • 23 | 24 |
  • 25 |
26 | 35 |
36 |
37 |
User not found
38 |
39 |
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 | --------------------------------------------------------------------------------