Skip to content

Commit 12fdbc0

Browse files
sanghoonioclaude
andcommitted
Refactor HN integration, add inline comments, atmospheric effects, and GitHub Pages deploy
Remove react-router-dom in favor of URL params SPA navigation. Consolidate duplicate Top/New/Best components into parameterized StoryList. Add native inline comment trees, user profile cards, atmospheric smoke overlay, and smoke animation infrastructure. Titles open in new tab, comments expand inline below stories. Add GitHub Actions deploy workflow. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ebadf5a commit 12fdbc0

23 files changed

+981
-394
lines changed

.github/workflows/deploy.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: Deploy to GitHub Pages
2+
3+
on:
4+
push:
5+
branches: [main]
6+
7+
permissions:
8+
contents: read
9+
pages: write
10+
id-token: write
11+
12+
concurrency:
13+
group: pages
14+
cancel-in-progress: true
15+
16+
jobs:
17+
build:
18+
runs-on: ubuntu-latest
19+
steps:
20+
- uses: actions/checkout@v4
21+
- uses: actions/setup-node@v4
22+
with:
23+
node-version: 20
24+
cache: npm
25+
- run: npm ci
26+
- run: npm run build
27+
- uses: actions/upload-pages-artifact@v3
28+
with:
29+
path: dist
30+
31+
deploy:
32+
needs: build
33+
runs-on: ubuntu-latest
34+
environment:
35+
name: github-pages
36+
url: ${{ steps.deployment.outputs.page_url }}
37+
steps:
38+
- id: deployment
39+
uses: actions/deploy-pages@v4

index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
5-
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
5+
<link rel="icon" type="image/png" href="/favicon.png" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
77
<title>Cigareditte</title>
88
</head>

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
"bootstrap-icons": "^1.13.1",
1818
"react": "^19.1.0",
1919
"react-dom": "^19.1.0",
20-
"react-router-dom": "^7.6.1",
2120
"zustand": "^5.0.5"
2221
},
2322
"devDependencies": {
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
---
2+
date: 2026-02-15
3+
status: complete
4+
description: Improve Cigareditte UX, HN API integration, and visual metaphor
5+
---
6+
7+
# Cigareditte Improvements
8+
9+
## Context
10+
11+
Cigareditte is a ~630-line React app that reframes HN browsing as smoking cigarettes (5-min timer per cigarette, pixel art sprites). The user wants three improvements: (1) better ease of use and more complete HN API integration, (2) native comment rendering instead of iframes for HN discussions, and (3) subtle atmospheric visual effects that make the page reflect the smoking state. Smoke animation will use image-model-generated assets (not CSS particles).
12+
13+
## Phase 1: Foundation Refactoring
14+
15+
No visual changes — clean up the code to unblock everything else.
16+
17+
### 1A. Shared types (`src/types.ts` — new)
18+
19+
Define `StoryType`, `HNItem`, `HNUser`, `AlgoliaHit`, `AlgoliaSearchResult` interfaces. Currently everything is `any`.
20+
21+
### 1B. Generalize queries (`src/queries/main.ts` — modify)
22+
23+
Replace three identical fetch functions + hooks with one parameterized pair:
24+
25+
```ts
26+
const STORY_ENDPOINTS: Record<StoryType, string> = {
27+
top: 'topstories', new: 'newstories', best: 'beststories',
28+
ask: 'askstories', show: 'showstories', job: 'jobstories',
29+
};
30+
31+
export const useStoryIds = (type: StoryType) => {
32+
const { totalSmoked } = useCigarette();
33+
return useQuery({
34+
queryKey: ['storyIds', type, totalSmoked],
35+
queryFn: () => getStoryIds(type),
36+
});
37+
};
38+
```
39+
40+
Add new hooks: `useUser(username)`, `useSearch(query, page)` (Algolia), `useCommentTree(kidIds)`.
41+
42+
Keep `useItem` and `useItems` as-is (they already work generically).
43+
44+
### 1C. Consolidate story pages (`src/components/StoryList.tsx` — new)
45+
46+
Replace Top.tsx, New.tsx, Best.tsx (three nearly identical ~60-line files) with one parameterized `StoryList` component that takes `type: StoryType`.
47+
48+
Extract per-item rendering into `StoryItem.tsx` — adds domain display (e.g. "(github.com)") next to titles.
49+
50+
### 1D. Utils cleanup (`src/utils.ts` — modify)
51+
52+
- Add `extractDomain(url)` for showing domains next to story links.
53+
- Remove unused `createSmoke()` (imperative DOM manipulation, doesn't fit React).
54+
55+
### 1E. Routing + navigation updates (`src/main.tsx`, `src/components/Navbar.tsx` — modify)
56+
57+
- Update routes to use `<StoryList type="..." />` instead of separate components.
58+
- Add routes: `/ask`, `/show`, `/jobs`, `/search`.
59+
- Add nav links for Ask HN, Show HN, Jobs, Search in both sidebar and mobile menu.
60+
- Extract duplicated 10-line cigarette sprite ternary chain into `getCigaretteSprite(burnProgress)` helper.
61+
62+
### 1F. Delete old files
63+
64+
Remove `Top.tsx`, `New.tsx`, `Best.tsx`.
65+
66+
**Files:** `types.ts` (new), `queries/main.ts`, `utils.ts`, `main.tsx`, `Navbar.tsx`, `StoryList.tsx` (new), `StoryItem.tsx` (new). Delete: `Top.tsx`, `New.tsx`, `Best.tsx`.
67+
68+
---
69+
70+
## Phase 2: Native Comments, User Profiles, Search
71+
72+
Depends on Phase 1 (uses generalized query hooks + StoryItem component).
73+
74+
### 2A. Recursive comment tree (`CommentTree.tsx`, `CommentItem.tsx` — new)
75+
76+
Lazy-loading approach: fetch top-level comments (story's `kids` array) immediately, fetch nested replies when parent renders. Auto-expand 3 levels deep, then show "[N replies]" link to expand further. Each `CommentItem` renders: username, relative time, collapse toggle, HTML text, and child `CommentTree`.
77+
78+
Comment items share the `['item', id]` cache key with story items (same API endpoint) — no duplicate caching logic.
79+
80+
### 2B. Modify SelectedItemView (`SelectedItemView.tsx` — modify)
81+
82+
Current: always renders an iframe. New dual-mode approach:
83+
84+
- **Article tab** (when story has external URL): iframe to external site.
85+
- **Comments tab**: native comment tree via `CommentTree`. Default when story has no URL (self-posts, Ask HN).
86+
- Toggle buttons in the toolbar. Fetch item data with `useItem` to get `kids` array.
87+
88+
### 2C. User profile card (`UserProfile.tsx` — new)
89+
90+
Lightweight positioned card shown on username click (not a full page). Shows karma, join date, bio snippet, link to HN profile. Triggered from `StoryItem` and `CommentItem`.
91+
92+
### 2D. Search page (`SearchView.tsx` — new)
93+
94+
Uses Algolia HN Search API (`hn.algolia.com/api/v1/search`). Search input, paginated results in the same format as story lists. Clicking a result opens it in `SelectedItemView`.
95+
96+
**Files:** `CommentTree.tsx` (new), `CommentItem.tsx` (new), `UserProfile.tsx` (new), `SearchView.tsx` (new), `SelectedItemView.tsx`, `style.css` (comment + user profile CSS).
97+
98+
---
99+
100+
## Phase 3: Atmospheric Visual Effects
101+
102+
Independent of Phase 2. Only needs Phase 1 (Zustand store access).
103+
104+
### 3A. Atmospheric overlay (`AtmosphericOverlay.tsx` — new)
105+
106+
Two `position: fixed; pointer-events: none` overlay divs driven by `burnProgress`:
107+
108+
- **Warm tint**: `rgba(180, 140, 80, progress * 0.06)` — amber overlay at extremely low opacity, building over 5 minutes. Barely perceptible at max.
109+
- **Vignette**: `box-shadow: inset 0 0 Npx rgba(160, 140, 110, progress * 0.12)` — soft warm haze closing in from edges. Blur radius grows with progress.
110+
111+
Both use `transition: all 2s ease` to smooth over the 1-second timer ticks. Renders `null` when `!isSmoking`, so effects disappear when cigarette ends and reset from zero on next light.
112+
113+
### 3B. Ember glow on cigarette sprite (`Navbar.tsx`, `style.css` — modify)
114+
115+
```css
116+
@keyframes ember-pulse {
117+
0%, 100% { filter: drop-shadow(0 0 4px rgba(255, 120, 30, 0.4)); }
118+
50% { filter: drop-shadow(0 0 8px rgba(255, 120, 30, 0.7)); }
119+
}
120+
.ember-glow { animation: ember-pulse 2.5s ease-in-out infinite; }
121+
```
122+
123+
Uses `filter: drop-shadow` (not `box-shadow`) because sprites have transparent backgrounds — `drop-shadow` follows the alpha contour.
124+
125+
### 3C. Mount overlay in `main.tsx`
126+
127+
Add `<AtmosphericOverlay />` as last child inside the smoking view container.
128+
129+
**Files:** `AtmosphericOverlay.tsx` (new), `Navbar.tsx`, `main.tsx`, `style.css`.
130+
131+
---
132+
133+
## Phase 4: Smoke Animation Infrastructure
134+
135+
Builds the system for plugging in image-model-generated smoke assets. Independent of Phases 2 and 3.
136+
137+
### 4A. Smoke layer component (`SmokeLayer.tsx` — new)
138+
139+
Supports two asset formats via a config object:
140+
141+
- **Animated image** (APNG/WebP/GIF): just an `<img>` tag. Simplest path — drop a looping smoke animation with transparent background into `public/`.
142+
- **Sprite sheet**: CSS `background-position` animation with `steps()`. Configurable frame count, dimensions, FPS.
143+
- **Frame sequence**: `requestAnimationFrame` loop swapping `src` on an `<img>`.
144+
145+
Positioned relative to the cigarette sprite via a wrapper div. `pointer-events: none` so it never blocks interaction. Renders `null` when `!isSmoking`.
146+
147+
### 4B. CSS for smoke positioning (`style.css` — modify)
148+
149+
Position `.smoke-layer` above the cigarette sprite using `position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%)`. Different positioning for mobile topbar vs desktop sidebar.
150+
151+
### 4C. Mount in Navbar (`Navbar.tsx` — modify)
152+
153+
Wrap each cigarette `<img>` in a `position: relative` container and render `<SmokeLayer />` above it. Applied in both sidebar and topbar sections.
154+
155+
### Asset workflow (not code — user does this separately)
156+
157+
1. Generate looping smoke animation with transparent background using image model.
158+
2. Export as animated WebP or APNG (~128x256px).
159+
3. Drop into `public/smoke.webp`.
160+
4. Update `SMOKE_CONFIG.src` in `SmokeLayer.tsx`.
161+
5. Adjust `.smoke-layer` dimensions in CSS.
162+
163+
**Files:** `SmokeLayer.tsx` (new), `Navbar.tsx`, `style.css`.
164+
165+
---
166+
167+
## Implementation Order
168+
169+
```
170+
Phase 1 (Foundation) ─┬─> Phase 2 (Comments, Search, Profiles)
171+
├─> Phase 3 (Atmospheric Effects)
172+
└─> Phase 4 (Smoke Infrastructure)
173+
```
174+
175+
Phases 2, 3, 4 are independent of each other. Recommended order: 1 → 3 → 4 → 2 (3 and 4 are small, high-impact; 2 is the largest).
176+
177+
## Verification
178+
179+
- **Phase 1**: `npm run build` succeeds. All 6 story categories load. Domain names show next to external links. Navigation works on desktop and mobile.
180+
- **Phase 2**: Click a story → Comments tab shows native threaded comments. Click username → profile card appears. Search page returns results and opens stories.
181+
- **Phase 3**: Light a cigarette. Over 5 minutes, page gets subtly warmer and edges slightly haze. Cigarette sprite has a soft orange pulsing glow. All effects disappear when cigarette ends.
182+
- **Phase 4**: Drop a test animated image into `public/smoke.webp` — it appears above the cigarette, looping, on both desktop and mobile. Does not block clicks.
183+
184+
## File Summary
185+
186+
| Action | File |
187+
|--------|------|
188+
| Create | `types.ts`, `StoryList.tsx`, `StoryItem.tsx`, `CommentTree.tsx`, `CommentItem.tsx`, `UserProfile.tsx`, `SearchView.tsx`, `AtmosphericOverlay.tsx`, `SmokeLayer.tsx` |
189+
| Modify | `queries/main.ts`, `utils.ts`, `main.tsx`, `Navbar.tsx`, `SelectedItemView.tsx`, `style.css` |
190+
| Delete | `Top.tsx`, `New.tsx`, `Best.tsx` |

public/.nojekyll

Whitespace-only changes.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { useCigarette } from '../stores/cigarette';
2+
3+
export function AtmosphericOverlay() {
4+
const { isSmoking, burnProgress } = useCigarette();
5+
6+
if (!isSmoking) return null;
7+
8+
const progress = burnProgress / 100;
9+
10+
return (
11+
<>
12+
<div
13+
className='atmospheric-tint'
14+
style={{
15+
position: 'fixed',
16+
top: 0,
17+
left: 0,
18+
right: 0,
19+
bottom: 0,
20+
pointerEvents: 'none',
21+
zIndex: 99998,
22+
backgroundColor: `rgba(180, 140, 80, ${progress * 0.06})`,
23+
transition: 'all 2s ease',
24+
}}
25+
/>
26+
<div
27+
className='atmospheric-vignette'
28+
style={{
29+
position: 'fixed',
30+
top: 0,
31+
left: 0,
32+
right: 0,
33+
bottom: 0,
34+
pointerEvents: 'none',
35+
zIndex: 99998,
36+
backgroundColor: 'transparent',
37+
boxShadow: `inset 0 0 ${progress * 200}px rgba(160, 140, 110, ${progress * 0.12})`,
38+
transition: 'all 2s ease',
39+
}}
40+
/>
41+
</>
42+
);
43+
}

src/components/Best.tsx

Lines changed: 0 additions & 62 deletions
This file was deleted.

0 commit comments

Comments
 (0)