Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 4 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# Hacker News
# HN Feed

This project creates a more intuitive, user friendly UI for Hacker News, with the goal of increasing usage and participation.

## Overview

HN Feed is designed to be a standalone front-end web-application, with built in PWA compatibility, as well as android and ios apps using Capacitor.
Comment on lines 3 to +7

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix minor wording and capitalisation in the overview.

These are small doc quality issues but user‑facing.

✍️ Suggested wording tweaks
-This project creates a more intuitive, user friendly UI for Hacker News, with the goal of increasing usage and participation.
+This project creates a more intuitive, user‑friendly UI for Hacker News, with the goal of increasing usage and participation.

-HN Feed is designed to be a standalone front-end web-application, with built in PWA compatibility, as well as android and ios apps using Capacitor.
+HN Feed is designed to be a standalone front‑end web application, with built‑in PWA compatibility, as well as Android and iOS apps using Capacitor.
🧰 Tools
🪛 LanguageTool

[misspelling] ~3-~3: This word is normally spelled with a hyphen.
Context: ... This project creates a more intuitive, user friendly UI for Hacker News, with the goal of in...

(EN_COMPOUNDS_USER_FRIENDLY)


[grammar] ~7-~7: A hyphen is missing in the adjective “built-in”.
Context: ...ndalone front-end web-application, with built in PWA compatibility, as well as android a...

(BUILT_IN_HYPHEN)

🤖 Prompt for AI Agents
In `@README.md` around lines 3 - 7, The Overview text has a few wording and
capitalization issues: change "user friendly" to "user-friendly", "front-end
web-application" to "frontend web application" (or "front-end web application")
and make "built in" -> "built-in", and capitalize platform names "Android" and
"iOS" (so the sentence reads something like: "HN Feed is designed to be a
standalone frontend web application with built-in PWA support, as well as
Android and iOS apps using Capacitor."); also consider replacing "PWA
compatibility" with "PWA support" for clarity.


This project is structured as a monorepo containing:

- **Frontend**: React SPA with TypeScript and Vite (`apps/client/`)
Expand Down Expand Up @@ -50,16 +52,9 @@ You can find a live demo at [https://hnfeed.pages.dev/]

### Testing

- Testing suite set up with vitest
- `pnpm test` - Run tests across all workspaces

## Development Workflow

1. **Start development servers**: Run `pnpm dev` to start both frontend and backend
2. **Frontend development**: The React app runs on `http://localhost:5173`
3. **Backend development**: The Express API runs on `http://localhost:3000`
4. **Code formatting**: Prettier and ESLint are configured with shared rules
5. **Type checking**: TypeScript configurations are shared across workspaces

## Project Structure

```md
Expand Down
101 changes: 101 additions & 0 deletions apps/client/android/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore

# Built application files
*.apk
*.aar
*.ap_
*.aab

# Files for the ART/Dalvik VM
*.dex

# Java class files
*.class

# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/

# Gradle files
.gradle/
build/

# Local configuration file (sdk path, etc)
local.properties

# Proguard folder generated by Eclipse
proguard/

# Log Files
*.log

# Android Studio Navigation editor temp files
.navigation/

# Android Studio captures folder
captures/

# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml

# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore

# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/

# Google Services (e.g. APIs or Firebase)
# google-services.json

# Freeline
freeline.py
freeline/
freeline_project_description.json

# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md

# Version control
vcs.xml

# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/

# Android Profiling
*.hprof

# Cordova plugins for Capacitor
capacitor-cordova-android-plugins

# Copied web assets
app/src/main/assets/public

# Generated Config files
app/src/main/assets/capacitor.config.json
app/src/main/assets/capacitor.plugins.json
app/src/main/res/xml/config.xml
2 changes: 2 additions & 0 deletions apps/client/android/app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/build/*
!/build/.npmkeep
54 changes: 54 additions & 0 deletions apps/client/android/app/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
apply plugin: 'com.android.application'

android {
namespace = "dev.pages.hnfeed"
compileSdk = rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "dev.pages.hnfeed"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
Comment on lines +19 to +24

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider enabling code minification for release builds.

With minifyEnabled false, the release APK will contain unobfuscated code, making it easier to reverse-engineer. For production apps, enabling ProGuard/R8 minification is recommended for both security (obfuscation) and APK size reduction.

If you intentionally want to keep it disabled for debugging purposes initially, consider adding a TODO comment to enable it before production release.

🤖 Prompt for AI Agents
In `@apps/client/android/app/build.gradle` around lines 19 - 24, The release build
currently has minifyEnabled set to false under buildTypes.release which leaves
the APK unobfuscated; change minifyEnabled to true and ensure proguardFiles
includes the default and your rules (the existing proguardFiles entry is fine)
so R8/ProGuard will run on the release build, then verify your
proguard-rules.pro covers any kept classes; if you intentionally want it off for
debugging, add a TODO comment next to minifyEnabled false (or behind the release
block) reminding to enable minification before production.

}

repositories {
flatDir{
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
}
}

dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation project(':capacitor-android')
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
implementation project(':capacitor-cordova-android-plugins')
}

apply from: 'capacitor.build.gradle'

try {
def servicesJSON = file('google-services.json')
if (servicesJSON.text) {
apply plugin: 'com.google.gms.google-services'
}
} catch(Exception e) {
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}
Comment on lines +47 to +54

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Improve google-services.json existence check.

Using file.text and catching exceptions for control flow is an anti-pattern. If the file doesn't exist, file.text throws a FileNotFoundException. Use exists() for a cleaner approach.

♻️ Proposed fix
-try {
-    def servicesJSON = file('google-services.json')
-    if (servicesJSON.text) {
-        apply plugin: 'com.google.gms.google-services'
-    }
-} catch(Exception e) {
-    logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
-}
+def servicesJSON = file('google-services.json')
+if (servicesJSON.exists()) {
+    apply plugin: 'com.google.gms.google-services'
+} else {
+    logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
def servicesJSON = file('google-services.json')
if (servicesJSON.text) {
apply plugin: 'com.google.gms.google-services'
}
} catch(Exception e) {
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}
def servicesJSON = file('google-services.json')
if (servicesJSON.exists()) {
apply plugin: 'com.google.gms.google-services'
} else {
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}
🤖 Prompt for AI Agents
In `@apps/client/android/app/build.gradle` around lines 47 - 54, The current
try/catch uses servicesJSON.text which throws if the file is missing; replace
that control flow by checking the file existence before reading: create the File
reference (servicesJSON = file('google-services.json')), call exists() on it,
and only then apply plugin 'com.google.gms.google-services' (and optionally read
text if needed); remove the catch block and the use of servicesJSON.text to
avoid FileNotFoundException and rely on a simple if (servicesJSON.exists())
check around the apply plugin call to preserve the existing logger.info message
when the file is absent.

19 changes: 19 additions & 0 deletions apps/client/android/app/capacitor.build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN

android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
}

apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {


}


if (hasProperty('postBuildExtras')) {
postBuildExtras()
}
21 changes: 21 additions & 0 deletions apps/client/android/app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.getcapacitor.myapp;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Package name mismatch with the application.

The test file is placed in package com.getcapacitor.myapp, but the actual application package is dev.pages.hnfeed (as defined in MainActivity.java). This appears to be leftover boilerplate from the Capacitor template.

Consider moving this file to apps/client/android/app/src/androidTest/java/dev/pages/hnfeed/ and updating the package declaration accordingly.

🤖 Prompt for AI Agents
In
`@apps/client/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java`
at line 1, The test's package declaration is wrong: update the package line in
ExampleInstrumentedTest.java from "package com.getcapacitor.myapp;" to match the
app package "dev.pages.hnfeed" and move the file into the corresponding test
package directory (androidTest/java/dev/pages/hnfeed) so the
ExampleInstrumentedTest class resolves correctly against MainActivity; ensure
any imports or references in ExampleInstrumentedTest remain valid after the
package change.


import static org.junit.Assert.*;

import android.content.Context;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;

/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {

@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();

assertEquals("com.getcapacitor.app", appContext.getPackageName());
}
Comment on lines +19 to +25

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Test assertion will fail due to incorrect expected package name.

The test expects "com.getcapacitor.app" but the actual application package is dev.pages.hnfeed. This test will fail when executed.

🔧 Proposed fix
     `@Test`
     public void useAppContext() throws Exception {
         // Context of the app under test.
         Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();

-        assertEquals("com.getcapacitor.app", appContext.getPackageName());
+        assertEquals("dev.pages.hnfeed", appContext.getPackageName());
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.getcapacitor.app", appContext.getPackageName());
}
`@Test`
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("dev.pages.hnfeed", appContext.getPackageName());
}
🤖 Prompt for AI Agents
In
`@apps/client/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java`
around lines 19 - 25, The test assertion in
ExampleInstrumentedTest::useAppContext uses the wrong expected package name;
update the assertEquals call that compares the expected package string to
appContext.getPackageName() to use the actual package "dev.pages.hnfeed" (locate
the assertEquals in method useAppContext and replace the expected value), or
alternatively derive the expected package from BuildConfig if you prefer a
dynamic check; ensure the variable appContext and the assertEquals call are
updated accordingly.

}
41 changes: 41 additions & 0 deletions apps/client/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
Comment on lines +4 to +10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider the implications of android:allowBackup="true".

With allowBackup="true", the app's data can be backed up to Google Drive and restored on other devices. For a Hacker News reader app this is likely acceptable, but if you plan to store sensitive data (e.g., authentication tokens), consider setting this to false or using android:fullBackupContent to exclude sensitive files.

🤖 Prompt for AI Agents
In `@apps/client/android/app/src/main/AndroidManifest.xml` around lines 4 - 10,
The AndroidManifest's application tag currently sets android:allowBackup="true",
which permits app data to be backed up and restored; decide whether to disable
it or restrict what gets backed up: either change android:allowBackup to "false"
on the application element or add a android:fullBackupContent attribute
referencing a backup rules XML and create that XML to exclude sensitive
files/tokens; update the application element (the <application> tag) to include
the chosen change and ensure the backup rules XML (if used) lists excluded paths
for sensitive data.


<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask"
android:exported="true">

<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

</activity>

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
</application>

<!-- Permissions -->

<uses-permission android:name="android.permission.INTERNET" />
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package dev.pages.hnfeed;

import com.getcapacitor.BridgeActivity;

public class MainActivity extends BridgeActivity {}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>
Comment on lines +1 to +34

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider customising the launcher icon.

This appears to be the default Android Studio launcher foreground drawable (the Android robot icon). For a production app, you should replace this with custom branding that represents the HN Feed app.

🤖 Prompt for AI Agents
In `@apps/client/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml`
around lines 1 - 34, The foreground drawable ic_launcher_foreground.xml
currently uses the default Android robot vector (vector element with path
children); replace it with your app's branded launcher foreground by either
importing a custom SVG and converting it to an Android vector drawable or
creating a new vector path that matches your logo, ensuring you update the
vector's viewport/width/height and any aapt:attr gradient items as needed;
regenerate adaptive icon layers (foreground and background) so the adaptive icon
references your new drawable and verify the result on multiple
densities/resolutions.

Loading
Loading