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
62 changes: 31 additions & 31 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,22 @@

---

## 🔥 Phase 1: Essential UX Foundations
*Timeline: 1-2 days | Priority: CRITICAL*
## Phase 1: Essential UX Foundations
*Timeline: 1-2 days | Priority: CRITICAL | **COMPLETED***

These features make the current experience sticky and shareable.

| Feature | Status | Priority | Effort | Value | Notes |
|---------|--------|----------|--------|-------|-------|
| **URL-Based Coordinate Sharing** | 📋 | 🔥 | ⚡ | ⭐⭐⭐⭐⭐ | Encode coords in URL for instant sharing |
| **Temporal Journal (Visit History)** | 📋 | 🔥 | ⚡⚡ | ⭐⭐⭐⭐⭐ | localStorage history of visited coords |
| **Image Gallery & Export** | 📋 | 🔥 | ⚡⚡ | ⭐⭐⭐⭐ | Save images to IndexedDB, download PNG |
| **URL-Based Coordinate Sharing** | | 🔥 | ⚡ | ⭐⭐⭐⭐⭐ | Encode coords in URL for instant sharing |
| **Temporal Journal (Visit History)** | | 🔥 | ⚡⚡ | ⭐⭐⭐⭐⭐ | localStorage history of visited coords |
| **Image Gallery & Export** | | 🔥 | ⚡⚡ | ⭐⭐⭐⭐ | Save images to IndexedDB, download PNG |

**Deliverables**:
- Share button with copy-to-clipboard
- Journal panel in left sidebar
- Gallery modal with grid view
- Download as PNG feature
- Share button with copy-to-clipboard
- Journal panel in left sidebar
- Gallery modal with grid view
- Download as PNG feature

---

Expand Down Expand Up @@ -111,31 +111,31 @@ Features that don't align with the app's vision:

## Current Sprint

**Active Sprint**: Phase 1 - Essential UX Foundations
**Start Date**: TBD
**Target Completion**: 1-2 days
**Active Sprint**: Phase 2 - Temporal Navigation
**Status**: Ready to start
**Previous Sprint**: Phase 1 - Essential UX Foundations ✅ COMPLETED

### Sprint Backlog
### Phase 1 Sprint Summary (Completed)

- [ ] 1.1 URL-Based Coordinate Sharing (2 hours)
- [ ] Create urlManager utility
- [ ] Integrate with ChronoscopeContext
- [ ] Add Share button to header
- [ ] Test with all coordinate types
- [x] 1.1 URL-Based Coordinate Sharing
- [x] Create urlManager utility
- [x] Integrate with ChronoscopeContext
- [x] Add Share button to header
- [x] Auto-update URL on scene render

- [ ] 1.2 Temporal Journal (4 hours)
- [ ] Create temporalJournal utility
- [ ] Build Journal component
- [ ] Add to left sidebar
- [ ] Implement export/import
- [ ] Test localStorage limits
- [x] 1.2 Temporal Journal
- [x] Create temporalJournal utility
- [x] Build Journal component
- [x] Add to left sidebar
- [x] Implement export/import
- [x] Auto-save on scene render

- [ ] 1.3 Image Gallery (6 hours)
- [ ] Set up IndexedDB schema
- [ ] Create imageGallery utility
- [ ] Build Gallery modal component
- [ ] Add download functionality
- [ ] Test storage limits
- [x] 1.3 Image Gallery
- [x] Set up IndexedDB schema with idb
- [x] Create galleryService utility
- [x] Build Gallery modal component
- [x] Add download functionality
- [x] Auto-save generated images

---

Expand Down Expand Up @@ -196,7 +196,7 @@ Track these KPIs to guide future development:
| Version | Date | Changes |
|---------|------|---------|
| 1.0.0 | 2025-11-28 | Initial app launch with core features |
| 1.1.0 | TBD | Phase 1: Essential UX (sharing, history, gallery) |
| 1.1.0 | 2025-11-28 | Phase 1: Essential UX (sharing, history, gallery) |
| 1.2.0 | TBD | Phase 2: Temporal navigation (slider, chat) |
| 2.0.0 | TBD | Phase 3: Enhanced discovery |

Expand Down
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
"idb": "^8.0.3",
"lucide-react": "^0.555.0",
"react": "^19.2.0",
"react-dom": "^19.2.0"
Expand Down
39 changes: 37 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { useState, useCallback } from 'react';
import { useState, useCallback, useEffect } from 'react';
import {
PanelLeftClose,
PanelLeftOpen,
PanelRightClose,
PanelRightOpen,
} from 'lucide-react';
import { ChronoscopeProvider } from './context/ChronoscopeContext';
import { ChronoscopeProvider, useChronoscope } from './context/ChronoscopeContext';
import {
Header,
ControlPlane,
Viewport,
DataStream,
Waypoints,
TemporalJournal,
} from './components';
import { getCoordinatesFromUrl, updateUrlWithCoordinates } from './utils/urlManager';
import { addJournalEntry } from './utils/temporalJournal';

interface ChronoscopeAppProps {
onApiKeyChange: () => void;
Expand All @@ -22,6 +25,36 @@ function ChronoscopeApp({ onApiKeyChange }: ChronoscopeAppProps) {
const [leftPanelOpen, setLeftPanelOpen] = useState(true);
const [rightPanelOpen, setRightPanelOpen] = useState(true);
const [mobileTab, setMobileTab] = useState<'controls' | 'viewport' | 'data'>('viewport');
const { state, setCoordinates, renderScene } = useChronoscope();

// Read URL coordinates on mount and auto-render
useEffect(() => {
const urlCoords = getCoordinatesFromUrl();
if (urlCoords) {
setCoordinates(urlCoords);
// Small delay to ensure state is set before rendering
setTimeout(() => {
renderScene();
}, 100);
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
Comment on lines +31 to +40

Choose a reason for hiding this comment

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

high

The use of setTimeout to delay renderScene is an anti-pattern in React and can lead to bugs. The renderScene function is closed over from the initial render and will not have the updated coordinates from setCoordinates, which can cause it to work with stale state. This is a race condition that might appear to work due to timing luck but is not reliable.

A more idiomatic and robust approach is to use separate useEffect hooks to first set the state, and then react to that state change to perform the render. This ensures you are always working with the latest state.

Consider replacing this useEffect with the following two hooks:

// Read URL coordinates on mount
useEffect(() => {
  const urlCoords = getCoordinatesFromUrl();
  if (urlCoords) {
    setCoordinates(urlCoords);
  }
}, [setCoordinates]);

// Auto-render scene from URL on initial load
useEffect(() => {
  const urlCoords = getCoordinatesFromUrl();
  // Only auto-render if there's no current scene and the input coordinates match the URL.
  if (urlCoords && !state.currentScene && JSON.stringify(state.inputCoordinates) === JSON.stringify(urlCoords)) {
    renderScene();
  }
}, [state.inputCoordinates, state.currentScene, renderScene]);

This change also allows you to remove the eslint-disable comment, as dependencies are correctly managed.


// Update URL and save to journal when scene is rendered
useEffect(() => {
if (state.currentScene) {
updateUrlWithCoordinates(state.currentScene.coordinates);

// Save to journal
addJournalEntry(
state.currentScene.coordinates,
state.currentScene.locationName,
!!state.generatedImage
);

// Notify journal component to refresh
window.dispatchEvent(new Event('journalUpdated'));
}
}, [state.currentScene]); // eslint-disable-line react-hooks/exhaustive-deps
Comment on lines +43 to +57

Choose a reason for hiding this comment

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

high

This effect has a bug related to journaling. It only runs when state.currentScene changes. If an image is generated for the current scene later (state.generatedImage changes), this effect does not re-run, and the journal entry is not updated to reflect hasGeneratedImage: true. Adding state.generatedImage to the dependency array would cause duplicate entries to be created with the current logic.

To fix this, addJournalEntry should be made idempotent for recent, identical coordinates. I've left a specific suggestion on src/utils/temporalJournal.ts to address this. Once that change is made, you can update this useEffect to correctly track image generation by adding state.generatedImage to the dependency array.

Suggested change
useEffect(() => {
if (state.currentScene) {
updateUrlWithCoordinates(state.currentScene.coordinates);
// Save to journal
addJournalEntry(
state.currentScene.coordinates,
state.currentScene.locationName,
!!state.generatedImage
);
// Notify journal component to refresh
window.dispatchEvent(new Event('journalUpdated'));
}
}, [state.currentScene]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
if (state.currentScene) {
updateUrlWithCoordinates(state.currentScene.coordinates);
// Save to journal
addJournalEntry(
state.currentScene.coordinates,
state.currentScene.locationName,
!!state.generatedImage
);
// Notify journal component to refresh
window.dispatchEvent(new Event('journalUpdated'));
}
}, [state.currentScene, state.generatedImage]);


return (
<div className="min-h-screen bg-chrono-black flex flex-col">
Expand Down Expand Up @@ -54,6 +87,7 @@ function ChronoscopeApp({ onApiKeyChange }: ChronoscopeAppProps) {
<div className="flex-1 overflow-y-auto p-4 space-y-4">
<ControlPlane />
<Waypoints />
<TemporalJournal />
</div>
)}
</div>
Expand Down Expand Up @@ -134,6 +168,7 @@ function ChronoscopeApp({ onApiKeyChange }: ChronoscopeAppProps) {
<div className="space-y-4">
<ControlPlane />
<Waypoints />
<TemporalJournal />
</div>
)}
{mobileTab === 'viewport' && <Viewport />}
Expand Down
69 changes: 69 additions & 0 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,16 @@ import {
Info,
X,
Github,
Share2,
Check,
Images,
} from 'lucide-react';
import { Settings } from './Settings';
import { ImageGallery } from './ImageGallery';
import { isGeminiConfigured } from '../services/geminiService';
import { getGalleryCount } from '../services/galleryService';
import { useChronoscope } from '../context/ChronoscopeContext';
import { copyShareableUrl } from '../utils/urlManager';

interface HeaderProps {
onApiKeyChange?: () => void;
Expand All @@ -17,7 +24,20 @@ export function Header({ onApiKeyChange }: HeaderProps) {
const [currentTime, setCurrentTime] = useState(new Date());
const [showInfo, setShowInfo] = useState(false);
const [showSettings, setShowSettings] = useState(false);
const [showGallery, setShowGallery] = useState(false);
const [apiConfigured, setApiConfigured] = useState(isGeminiConfigured());
const [galleryCount, setGalleryCount] = useState(0);
const [copied, setCopied] = useState(false);
const { state } = useChronoscope();

const handleShare = async () => {
if (!state.currentScene) return;
const success = await copyShareableUrl(state.currentScene.coordinates);
if (success) {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};

// Update real-world time every second
useEffect(() => {
Expand All @@ -27,6 +47,19 @@ export function Header({ onApiKeyChange }: HeaderProps) {
return () => clearInterval(timer);
}, []);

// Load gallery count on mount and listen for updates
useEffect(() => {
const loadCount = async () => {
const count = await getGalleryCount();
setGalleryCount(count);
};
loadCount();

// Listen for gallery update events
window.addEventListener('galleryUpdated', loadCount);
return () => window.removeEventListener('galleryUpdated', loadCount);
}, []);

return (
<>
<header className="bg-chrono-panel/90 backdrop-blur-sm border-b border-chrono-border px-4 py-3">
Expand Down Expand Up @@ -75,6 +108,32 @@ export function Header({ onApiKeyChange }: HeaderProps) {

{/* Actions */}
<div className="flex items-center gap-2">
<button
onClick={handleShare}
disabled={!state.currentScene}
className={`p-2 transition-colors ${
state.currentScene
? copied
? 'text-chrono-green'
: 'text-chrono-text-dim hover:text-chrono-blue'
: 'text-chrono-text-dim/30 cursor-not-allowed'
}`}
title={copied ? 'Link copied!' : state.currentScene ? 'Share coordinates' : 'Render a scene first'}
>
{copied ? <Check className="w-5 h-5" /> : <Share2 className="w-5 h-5" />}
</button>
<button
onClick={() => setShowGallery(true)}
className="relative p-2 text-chrono-text-dim hover:text-chrono-green transition-colors"
title="Image Gallery"
>
<Images className="w-5 h-5" />
{galleryCount > 0 && (
<span className="absolute -top-0.5 -right-0.5 min-w-[1rem] h-4 px-1 flex items-center justify-center text-[10px] font-mono rounded-full bg-chrono-green text-chrono-black">
{galleryCount > 99 ? '99+' : galleryCount}
</span>
)}
</button>
<button
onClick={() => setShowInfo(true)}
className="p-2 text-chrono-text-dim hover:text-chrono-blue transition-colors"
Expand Down Expand Up @@ -115,6 +174,16 @@ export function Header({ onApiKeyChange }: HeaderProps) {
}}
/>

{/* Image Gallery Modal */}
<ImageGallery
isOpen={showGallery}
onClose={() => {
setShowGallery(false);
// Refresh count when closing
getGalleryCount().then(setGalleryCount);
}}
/>

{/* Info Modal */}
{showInfo && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-chrono-black/80 backdrop-blur-sm">
Expand Down
Loading