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
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
VITE_SUPABASE_URL=https://your-project-id.supabase.co
VITE_SUPABASE_ANON_KEY=your_anon_key_here

# Your Anthropic API Key (for AI features)
VITE_ANTHROPIC_API_KEY=your_anthropic_api_key_here

# Your Gemini API Key (for AI features)
VITE_GEMINI_API_KEY=your_gemini_api_key_here

# 📋 Setup Instructions:
# 1. Copy this file: cp .env.example .env
# 2. Go to your Supabase project dashboard
Expand Down
105 changes: 105 additions & 0 deletions Integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Report Route — Integration Guide

Three things to wire up in the existing codebase. All changes are
additive — nothing existing should need modification.

---

## 1. Copy the new files

```markdown
src/utils/reportUtils.ts ← week grouping, prompt serialization
src/hooks/useReportSummary.ts ← API call + generation state
src/pages/Report.tsx ← the /report page component
```

---

## 2. Add the route in App.tsx

The project uses React Router v6 with lazy-loaded pages.
Find where the other pages are lazy-imported and add:

```tsx
const Report = lazy(() => import('./pages/Report'));
```

Then in the `<Routes>` block, add alongside the other `<Route>` entries:

```tsx
<Route path="/report" element={<Report />} />
```

---

## 3. Add navigation link

Find `Navigation.tsx` (desktop) and `MobileNav.tsx` (mobile bottom nav).
Add a "Report" entry following the same pattern as the existing nav items.

In Navigation.tsx, alongside the other nav links:

```tsx
<NavLink to="/report">Report</NavLink>
```

In MobileNav.tsx, the bottom nav likely maps an array of items.
Add an entry like:

```tsx
{ to: "/report", label: "Report", icon: <FileTextIcon /> }
```

Use whatever icon fits — `FileTextIcon` from Radix or Lucide both work.

---

## 4. Add the API key to .env

```env
VITE_ANTHROPIC_API_KEY=your_key_here
```

And add the placeholder to `.env.example`:

```env
VITE_ANTHROPIC_API_KEY=
```

The key is read in `useReportSummary.ts` via `import.meta.env.VITE_ANTHROPIC_API_KEY`.
The `anthropic-dangerous-direct-browser-access` header is required for
browser-based API calls — this is expected and documented by Anthropic for
client-side usage.

---

## 5. Verify TypeScript is happy

Run `npm run lint` and `npm run build` after dropping the files in.

The main thing to verify is that the `ArchivedDay` and `Task` types in
`reportUtils.ts` match what your actual `dataService.ts` types look like.
If `dataService.ts` already exports these types, import from there instead
of re-declaring them in `reportUtils.ts` to avoid duplication.

Check for a `TimeEntry`, `DayData`, or similar interface in:

- `src/services/dataService.ts`
- `src/contexts/TimeTrackingContext.tsx`

If those types exist, update the import at the top of `reportUtils.ts`:

```ts
import type { ArchivedDay, Task } from '@/services/dataService';
// or wherever they live
```

---

## Assumptions to verify

- `timetracker_archived_days` is the correct localStorage key ✓ (confirmed via console)
- Tasks have a `category` field with values like `"break-time"` ✓ (confirmed via console)
- shadcn/ui components used: `Button`, `Textarea`, `Badge`, `Tabs`, `Popover`, `Label`, `Input`
→ All are already in use in the project per the README tech stack
- `@radix-ui/react-icons` is available ✓ (listed as primary icon library in README)
6 changes: 3 additions & 3 deletions dev-dist/sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ if (!self.define) {
const singleRequire = (uri, parentUri) => {
uri = new URL(uri + ".js", parentUri).href;
return registry[uri] || (

new Promise(resolve => {
if ("document" in self) {
const script = document.createElement("script");
Expand All @@ -35,7 +35,7 @@ if (!self.define) {
resolve();
}
})

.then(() => {
let promise = registry[uri];
if (!promise) {
Expand Down Expand Up @@ -79,7 +79,7 @@ define(['./workbox-21a80088'], (function (workbox) { 'use strict';
*/
workbox.precacheAndRoute([{
"url": "index.html",
"revision": "0.iqaev82ti8g"
"revision": "0.n3k1cm3l0gs"
}], {});
workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
Expand Down
13 changes: 12 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Toaster } from "@/components/ui/toaster";
import { Toaster as Sonner } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import { AuthProvider } from "@/contexts/AuthContext";
import { OfflineProvider } from "@/contexts/OfflineContext";
import { TimeTrackingProvider } from "@/contexts/TimeTrackingContext";
import { useAuth } from "@/hooks/useAuth";
import { Suspense, lazy } from "react";
import { InstallPrompt } from "@/components/InstallPrompt";
import { UpdateNotification } from "@/components/UpdateNotification";
Expand All @@ -17,6 +18,7 @@ const ProjectList = lazy(() => import("./pages/ProjectList"));
const Settings = lazy(() => import("./pages/Settings"));
const NotFound = lazy(() => import("./pages/NotFound"));
const Categories = lazy(() => import("./pages/Categories"));
const Report = lazy(() => import('./pages/Report'));

// Loading fallback component
const PageLoader = () => (
Expand All @@ -25,6 +27,14 @@ const PageLoader = () => (
</div>
);

// Redirects unauthenticated users to home
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
const { isAuthenticated, loading } = useAuth();
if (loading) return <PageLoader />;
if (!isAuthenticated) return <Navigate to="/" replace />;
return <>{children}</>;
};

const App = () => (
<OfflineProvider>
<AuthProvider>
Expand All @@ -40,6 +50,7 @@ const App = () => (
<Route path="/categories" element={<Categories />} />
<Route path="/archive" element={<Archive />} />
<Route path="/settings" element={<Settings />} />
<Route path="/report" element={<ProtectedRoute><Report /></ProtectedRoute>} />
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
<Route path="*" element={<NotFound />} />
</Routes>
Expand Down
16 changes: 9 additions & 7 deletions src/components/MobileNav.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { memo } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Home, Archive, FolderKanban, Settings } from 'lucide-react';
import { Home, Archive, FolderKanban, Settings, PaperclipIcon } from 'lucide-react';
import { useAuth } from '@/hooks/useAuth';

export const MobileNav = memo(function MobileNav() {
const location = useLocation();
const { isAuthenticated } = useAuth();

const isActive = (path: string) => {
return location.pathname === path;
Expand All @@ -15,11 +17,11 @@ export const MobileNav = memo(function MobileNav() {
icon: Home,
label: 'Home'
},
{
path: '/projectlist',
icon: FolderKanban,
label: 'Projects'
},
...(isAuthenticated ? [{
path: '/report',
icon: PaperclipIcon,
label: 'Report'
}] : []),
{
path: '/archive',
icon: Archive,
Expand All @@ -39,7 +41,7 @@ export const MobileNav = memo(function MobileNav() {
paddingBottom: 'max(env(safe-area-inset-bottom, 0px), 0px)'
}}
>
<div className="grid grid-cols-4 h-16">
<div className={`grid ${navItems.length === 3 ? 'grid-cols-3' : 'grid-cols-4'} h-16`}>
{navItems.map(({ path, icon: Icon, label }) => (
<Link
key={path}
Expand Down
7 changes: 7 additions & 0 deletions src/components/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ const SiteNavigationMenu = () => {
onRefresh={forceSyncToDatabase}
/>
</Item>
{isAuthenticated && (
<Item className="hidden md:flex">
<NavLink to="/report" className={({ isActive }) =>
`transition-all duration-200 flex items-center space-x-2 px-4 rounded-md h-10 bg-white border border-gray-200 hover:bg-accent hover:accent-foreground hover:border-input ... ${isActive ? 'bg-blue-200 hover:bg-accent hover:text-accent-foreground' : 'bg-white'}`
}>Report</NavLink>
</Item>
)}
<Item className="hidden md:flex">
<Button
onClick={handlePrint}
Expand Down
123 changes: 123 additions & 0 deletions src/hooks/.useReportSummary-Claude.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// src/hooks/useReportSummary.ts
// Manages the AI summary generation lifecycle for the /report route.
// Calls the Anthropic API directly from the browser using the fetch API.
//
// ASSUMPTION: VITE_ANTHROPIC_API_KEY is set in your .env file.
// Add to .env.example: VITE_ANTHROPIC_API_KEY=your_key_here
// Never commit the actual key.
//
// TODO: Move API calls to a Cloudflare Worker proxy so the key is never
// exposed in the browser bundle. The Worker holds ANTHROPIC_API_KEY as a
// secret and this hook calls the Worker URL instead of api.anthropic.com.
// Remove VITE_ANTHROPIC_API_KEY and the anthropic-dangerous-direct-browser-access
// header once the proxy is in place.

import { useState, useCallback } from "react";
import {
WeekGroup,
ReportTone,
buildSummaryPrompt,
} from "@/utils/reportUtils";

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

export type GenerationState =
| "idle"
| "loading"
| "success"
| "error";

export interface UseReportSummaryReturn {
summary: string;
state: GenerationState;
error: string | null;
generate: (week: WeekGroup, tone: ReportTone) => Promise<void>;
updateSummary: (value: string) => void;
reset: () => void;
}

// ---------------------------------------------------------------------------
// Hook
// ---------------------------------------------------------------------------

export function useReportSummary(): UseReportSummaryReturn {
const [summary, setSummary] = useState<string>("");
const [state, setState] = useState<GenerationState>("idle");
const [error, setError] = useState<string | null>(null);

const generate = useCallback(
async (week: WeekGroup, tone: ReportTone) => {
setState("loading");
setError(null);
setSummary("");

const apiKey = import.meta.env.VITE_ANTHROPIC_API_KEY as string;

if (!apiKey) {
setState("error");
setError(
"Anthropic API key not found. Add VITE_ANTHROPIC_API_KEY to your .env file."
);
return;
}

try {
const { system, userMessage } = buildSummaryPrompt(week, tone);

const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": apiKey,
"anthropic-version": "2023-06-01",
// Required for browser-based calls
"anthropic-dangerous-direct-browser-access": "true",
},
body: JSON.stringify({
model: "claude-sonnet-4-20250514",
max_tokens: 512,
system,
messages: [{ role: "user", content: userMessage }],
}),
});

if (!response.ok) {
const errBody = await response.json().catch(() => ({}));
throw new Error(
errBody?.error?.message ?? `API error ${response.status}`
);
}

const data = await response.json();
const text: string =
data.content
?.filter((b: { type: string }) => b.type === "text")
.map((b: { text: string }) => b.text)
.join("") ?? "";

setSummary(text.trim());
setState("success");
} catch (err) {
setState("error");
setError(
err instanceof Error ? err.message : "An unexpected error occurred."
);
}
},
[]
);

const updateSummary = useCallback((value: string) => {
setSummary(value);
}, []);

const reset = useCallback(() => {
setSummary("");
setState("idle");
setError(null);
}, []);

return { summary, state, error, generate, updateSummary, reset };
}
Loading
Loading