From 0db8043fbc92b7003f2510668c8253f68b443622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Hameau?= Date: Tue, 16 Jun 2026 23:09:48 +0200 Subject: [PATCH 01/20] feat: pivot to point-to-point planning, add multi-page navigation, and seo/analytics enhancements --- README.md | 89 +++++------------ specs/completed/ui-seo-analytics-v1.md | 104 ++++++++++++++++++++ src/app/about/page.tsx | 81 +++++++++++++++ src/app/how-to/page.tsx | 89 +++++++++++++++++ src/app/layout.tsx | 59 +++++++++-- src/app/page.tsx | 92 +++++++++--------- src/app/robots.ts | 13 +++ src/app/sitemap.ts | 28 ++++++ src/components/Footer.tsx | 41 ++++++++ src/components/GenerationForm.test.tsx | 2 +- src/components/GenerationForm.tsx | 4 +- src/components/Navbar.test.tsx | 4 +- src/components/Navbar.tsx | 119 +++++++++++++++++------ src/components/NavbarWrapper.tsx | 15 +++ src/components/TripHistory.test.tsx | 58 +++++++++++ src/components/TripHistory.tsx | 60 ++++++++++++ src/components/WorkspaceActions.test.tsx | 8 +- src/components/WorkspaceActions.tsx | 30 +++--- src/hooks/useApp.tsx | 50 ++++++++++ src/services/ai.ts | 41 ++++---- src/utils/analytics.ts | 30 ++++++ 21 files changed, 833 insertions(+), 184 deletions(-) create mode 100644 specs/completed/ui-seo-analytics-v1.md create mode 100644 src/app/about/page.tsx create mode 100644 src/app/how-to/page.tsx create mode 100644 src/app/robots.ts create mode 100644 src/app/sitemap.ts create mode 100644 src/components/Footer.tsx create mode 100644 src/components/NavbarWrapper.tsx create mode 100644 src/components/TripHistory.test.tsx create mode 100644 src/components/TripHistory.tsx create mode 100644 src/hooks/useApp.tsx create mode 100644 src/utils/analytics.ts diff --git a/README.md b/README.md index ff36d35..8c8e722 100644 --- a/README.md +++ b/README.md @@ -5,39 +5,33 @@ [![Hosting](https://img.shields.io/badge/Hosted_on-GitHub_Pages-blue?logo=github)](https://gehdoc.github.io/quick-tripper) [![Framework](https://img.shields.io/badge/Built_with-React-61DAFB?logo=react)](https://react.dev) [![AI](https://img.shields.io/badge/AI_Model-Llama_3-7B1FA2?logo=meta)](https://huggingface.co) +[![License](https://img.shields.io/badge/License-MIT-green)](./LICENSE) -**Quick-tripper** is a privacy-first, zero-backend travel companion web app that generates detailed road trip itineraries using AI. It operates entirely in your browser—no databases, no logins, no tracking. Now powered by **Llama 3.1** via the Hugging Face Serverless API. +**Quick-tripper** is a privacy-first, zero-backend "Point-to-Point" trip planner. Instead of overwhelming you with complex itineraries, it helps you plan specific journeys one-by-one—from a starting point to an arrival—leveraging native Google Maps route proposals and AI-powered arrival formatting. ## ✨ Features -- **🌍 Smart Itineraries**: Generate day-by-day travel plans using Llama 3.1. -- **🗺️ Interactive Maps**: Embedded Google Maps for every journey, rendered securely from AI-generated routes. -- **🔒 Privacy-First**: "Bring Your Own Key" (BYOK) model. Your API token is stored locally in your browser and never sent to our servers. -- **🔗 Compressed Sharing**: Share your entire itinerary via a single, ultra-compressed URL (powered by LZString). -- **💾 Local Persistence**: Automatic synchronization with your browser's local storage. -- **🛡️ Secure Rendering**: Strict markdown sanitization to prevent XSS and ensure your browser environment remains safe. +- **📍 Point-to-Point Planning**: Focus on one trip at a time. Define where you start and where you want to go. +- **🗺️ Native Route Proposals**: Directly integrates with Google Maps to provide the best routes, alternatives, and live navigation. +- **📝 Formatted Arrival Insights**: Use your own notes about the arrival or point of interest; our AI ensures they are cleanly formatted and grammatically correct. +- **🔒 Privacy-First**: "Bring Your Own Key" (BYOK) model. Your API token and trip data stay in your browser. No databases, no tracking, no accounts. +- **🔗 Compressed Sharing**: Share any trip via a single, ultra-compressed URL (powered by LZString). +- **💾 Local Workspace**: Your trip history is automatically synchronized with your browser's local storage. -## 📖 How-to Use - -1. **Enter your Hugging Face API Token**: Get a free "User Access Token" from your [Hugging Face Settings](https://huggingface.co/settings/tokens). -2. **Describe your trip**: Use the prompt area to describe where you want to go, for how long, and what you like. -3. **Generate**: Click "Generate Itinerary" and wait a few seconds. -4. **Explore**: Use the interactive map and read the day-by-day suggestions. -5. **Manage**: Your trips are saved automatically. You can export them as JSON or share them via a unique link. - -## 🔒 Privacy & BYOK (Bring Your Own Key) +## 🔒 The "Free & Private" Philosophy -Quick-tripper is designed to be **serverless and private**. We don't have a backend to store your data or your keys. +Quick-tripper is built on three core pillars: -- **Local Storage**: Your API token and trip history are stored ONLY in your browser's local storage. -- **Direct AI Calls**: The app communicates directly with Hugging Face APIs from your browser. -- **No Tracking**: We don't use cookies, analytics, or tracking scripts. +1. **Free to Use**: Open-source under the MIT license. It leverages free-tier AI inference via Hugging Face. +2. **Privacy as a Right**: We don't want your data. There is no backend, no login, and no telemetry. +3. **User Empowerment**: You bring your own API key, giving you full control over your AI usage and privacy. -To use the app, you need a **Hugging Face User Access Token**. +## 📖 How-to Use -- Visit [Hugging Face Settings](https://huggingface.co/settings/tokens). -- Create a new token (Read access is enough). -- Paste it into the top bar of Quick-tripper. +1. **Enter your Hugging Face API Token**: Get a free "User Access Token" from [Hugging Face](https://huggingface.co/settings/tokens). +2. **Plan your trip**: Enter your starting point and arrival. Add some notes about what you want to do at the destination. +3. **Generate**: Click "Plan Trip". The AI will extract the locations and format your notes. +4. **Navigate**: Use the direct Google Maps link to explore route proposals and start your journey. ## 🛠️ Tech Stack @@ -45,56 +39,23 @@ To use the app, you need a **Hugging Face User Access Token**. - **Styling**: [TailwindCSS 4](https://tailwindcss.com) + [DaisyUI 5](https://daisyui.com) - **AI Integration**: [Llama 3.1 8B](https://huggingface.co/meta-llama/Llama-3.1-8B-Instruct) via Hugging Face Router. - **Data Compression**: [LZString](https://pieroxy.net/lua/lz-string/index.html) -- **Testing**: [Vitest](https://vitest.dev) + [React Testing Library](https://testing-library.com) -- **Automation**: [Husky](https://typicode.github.io/husky/) + [lint-staged](https://github.com/lint-staged/lint-staged) +- **Analytics**: [Umami](https://umami.is) (Privacy-focused, cookieless) ## 🔄 Development Workflow (SDD) -This project follows **Spec-Driven Development (SDD)** to maintain a clear roadmap and high technical standards. Before implementing a new feature, a technical specification must be drafted and approved. - -- **For Contributors**: See **[CONTRIBUTING.md](./CONTRIBUTING.md)** for workflow and commands. -- **For AI Agents**: Follow the **[AGENTS.md](./AGENTS.md)** protocol. +This project follows **Spec-Driven Development (SDD)**. See **[AGENTS.md](./AGENTS.md)** for protocol. ## 🚀 Getting Started -1. **Clone the Repo**: - ```bash - git clone https://github.com/gehdoc/quick-tripper.git - cd quick-tripper - ``` -2. **Install Dependencies**: - ```bash - npm install - ``` -3. **Run Locally**: - ```bash - npm run dev - ``` -4. **Open the App**: Visit `http://localhost:3000` (development) or `http://localhost:3000/quick-tripper` (production-emulated) and enter your API key in the top bar. +```bash +npm install +npm run dev +``` ## 🏗️ Architecture -For a detailed look at the system design, core modules, and data contract versioning, see: **[docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md)**. - -## 🧪 Quality & Integrity Strategy - -To maintain a robust "zero-backend" application, we rely on a multi-tiered validation strategy: - -1. **Architectural Design**: High-level designs and data contracts are documented in `ARCHITECTURE.md`. -2. **Static Analysis**: ESLint and Prettier ensure code consistency and catch early errors. -3. **Type Safety**: Strict TypeScript is enforced at the commit level via Husky hooks. -4. **Unit Testing**: Vitest and JSDOM validate core logic, utility functions, and presentational components (co-located tests). -5. **E2E Testing**: Playwright validates full user workflows and "golden paths" in a real browser environment. -6. **Spec-First Implementation**: Every change is traced back to a technical specification in `specs/`, ensuring architectural alignment. +For a detailed look at the system design, see: **[docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md)**. ## 📄 License MIT © [Quick-tripper](https://github.com/gehdoc/quick-tripper) - -## 💖 Support the Project - -If you find this tool helpful, please consider supporting its development: - -[![Donate](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://paypal.me/GehDoc) - -Your support helps cover maintenance and further development of the tool. Thank you! diff --git a/specs/completed/ui-seo-analytics-v1.md b/specs/completed/ui-seo-analytics-v1.md new file mode 100644 index 0000000..71eddbf --- /dev/null +++ b/specs/completed/ui-seo-analytics-v1.md @@ -0,0 +1,104 @@ +# Spec: UI, SEO, and Analytics Enhancements (v1) + +This specification outlines the improvements for desktop UI, general UI refinements, Umami analytics integration, SEO optimization, and a core pivot of the project goal towards "Point-to-Point" trip planning. + +## 1. Problem Statement + +The current UI is functional but lacks a "desktop-first" feel. More importantly, the project goal needs to be refined: instead of general itineraries, users want to plan specific trips from a starting point to an arrival, focusing on route proposals and arrival descriptions with minimal AI intervention. Additionally, there is no visitor tracking and minimal SEO metadata. + +## 2. Proposed Changes + +### 2.0 Core Mission Pivot: Point-to-Point Planning + +- **Goal Update**: Quick-tripper is now a tool to plan trips one-by-one, from a starting point to an arrival (or point of interest). +- **AI Prompt Refinement**: + - **Extraction Only**: AI focuses strictly on extracting/validating "Starting Point" and "Arrival" from user input. + - **No AI Route Generation**: AI MUST NOT generate route descriptions or suggestions. Its only "map" task is to generate the correct Google Maps URL format using the extracted locations. + - **Description Formatting**: "Arrival Description" should strictly follow user input, with only grammatical/syntax corrections and markdown formatting. +- **Google Maps Centrality**: Leverage Google Maps' native route proposal capabilities by ensuring the generated links use optimal parameters for multi-route suggestions. +- **README Update**: Rewrite the project description to align with this new focused goal. + +### 2.1 UI/UX Refinements + +- **Desktop Layout**: + - Implement a responsive layout using a sidebar for desktop (`lg:flex lg:flex-row`). + - Sidebar will house `WorkspaceActions` and a new `TripHistory` component (refactored from `TripNavigator`). + - Main area will house `Navbar`, `GenerationForm`, and `TripViewer`. +- **Generation Form**: + - Refactor to potentially include explicit fields for "Start" and "Arrival" to improve accuracy, or keep it as a single prompt but with clearer instructions. + - Update CTA to "Plan Trip". +- **Aesthetics**: + - Update `TripViewer` with better typography and spacing. + - Enhance `GenerationForm` with a more prominent CTA. + - Improve transitions and hover states across the app. + +### 2.2 Umami Analytics Integration + +- Add Umami tracking script to `src/app/layout.tsx`. +- Implement event tracking for: + - `trip_planned`: When a user successfully generates a trip. + - `trip_shared`: When the share button is clicked. + - `trip_exported`: When the export button is clicked. + - `trip_deleted`: When a trip is deleted. + +### 2.3 SEO Optimization + +- Expand the `metadata` object in `src/app/layout.tsx`: + - Add `openGraph` and `twitter` card support. + - Refine `title` and `description` for better CTR. + - Add `keywords` (e.g., "Private AI Travel", "Point-to-Point Planner", "Zero-Backend Trip Planner"). +- Ensure semantic HTML tags (e.g., `
`, `
); } diff --git a/src/app/robots.ts b/src/app/robots.ts new file mode 100644 index 0000000..76f6c53 --- /dev/null +++ b/src/app/robots.ts @@ -0,0 +1,13 @@ +import { MetadataRoute } from 'next'; + +export const dynamic = 'force-static'; + +export default function robots(): MetadataRoute.Robots { + return { + rules: { + userAgent: '*', + allow: '/', + }, + sitemap: 'https://gehdoc.github.io/quick-tripper/sitemap.xml', + }; +} diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts new file mode 100644 index 0000000..1247059 --- /dev/null +++ b/src/app/sitemap.ts @@ -0,0 +1,28 @@ +import { MetadataRoute } from 'next'; + +export const dynamic = 'force-static'; + +export default function sitemap(): MetadataRoute.Sitemap { + const baseUrl = 'https://gehdoc.github.io/quick-tripper'; + + return [ + { + url: baseUrl, + lastModified: new Date(), + changeFrequency: 'monthly', + priority: 1, + }, + { + url: `${baseUrl}/about`, + lastModified: new Date(), + changeFrequency: 'monthly', + priority: 0.8, + }, + { + url: `${baseUrl}/how-to`, + lastModified: new Date(), + changeFrequency: 'monthly', + priority: 0.8, + }, + ]; +} diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx new file mode 100644 index 0000000..032001c --- /dev/null +++ b/src/components/Footer.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import Link from 'next/link'; +import { IoHeart } from 'react-icons/io5'; + +export const Footer: React.FC = () => { + return ( + + ); +}; diff --git a/src/components/GenerationForm.test.tsx b/src/components/GenerationForm.test.tsx index 885edd3..1c423f7 100644 --- a/src/components/GenerationForm.test.tsx +++ b/src/components/GenerationForm.test.tsx @@ -45,7 +45,7 @@ describe('GenerationForm', () => { isLoading={false} />, ); - const button = screen.getByText(/Generate Itinerary/i); + const button = screen.getByText(/Plan Trip/i); fireEvent.click(button); expect(handleGenerate).toHaveBeenCalled(); }); diff --git a/src/components/GenerationForm.tsx b/src/components/GenerationForm.tsx index a243a5c..a568929 100644 --- a/src/components/GenerationForm.tsx +++ b/src/components/GenerationForm.tsx @@ -33,13 +33,13 @@ export const GenerationForm: React.FC = ({ diff --git a/src/components/Navbar.test.tsx b/src/components/Navbar.test.tsx index 293bc8d..266be90 100644 --- a/src/components/Navbar.test.tsx +++ b/src/components/Navbar.test.tsx @@ -12,7 +12,7 @@ describe('Navbar', () => { it('renders the API key input with correct value', () => { render( {}} />); - const input = screen.getByPlaceholderText(/HuggingFace API Token/i); + const input = screen.getByPlaceholderText(/HF Token/i); expect(input).toBeInTheDocument(); expect(input).toHaveValue('test-key'); }); @@ -20,7 +20,7 @@ describe('Navbar', () => { it('calls onApiKeyChange when input changes', () => { const handleChange = vi.fn(); render(); - const input = screen.getByPlaceholderText(/HuggingFace API Token/i); + const input = screen.getByPlaceholderText(/HF Token/i); fireEvent.change(input, { target: { value: 'new-key' } }); expect(handleChange).toHaveBeenCalledWith('new-key'); diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index bfe9117..fd631c3 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,45 +1,108 @@ import React from 'react'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; import { FaKey as Key } from 'react-icons/fa'; import { SiGithub as GitHub } from 'react-icons/si'; +import { IoInformationCircleOutline, IoBookOutline, IoMapOutline } from 'react-icons/io5'; import { VERSION, REPO_URL } from '@/utils/version'; import { Logo } from '@/components/Logo'; interface NavbarProps { - apiKey: string; - onApiKeyChange: (value: string) => void; + apiKey?: string; + onApiKeyChange?: (value: string) => void; } export const Navbar: React.FC = React.memo(({ apiKey, onApiKeyChange }) => { + const pathname = usePathname(); + + const navLinks = [ + { href: '/', label: 'Planner', icon: }, + { href: '/about', label: 'About', icon: }, + { href: '/how-to', label: 'How-to', icon: }, + ]; + return ( -
-
- -
- Quick-tripper -
- v{VERSION} - - GitHub - +
+
+ + +
+ Quick-tripper + v{VERSION}
+ + +
+ {navLinks.map((link) => ( + + {link.icon} + {link.label} + + ))}
-
- + +
+ {onApiKeyChange && ( +
+ +
+ )} + + + + + + {/* Mobile Nav Toggle */} +
+ +
    + {navLinks.map((link) => ( +
  • + + {link.icon} {link.label} + +
  • + ))} +
+
); diff --git a/src/components/NavbarWrapper.tsx b/src/components/NavbarWrapper.tsx new file mode 100644 index 0000000..1630afe --- /dev/null +++ b/src/components/NavbarWrapper.tsx @@ -0,0 +1,15 @@ +'use client'; + +import React from 'react'; +import { Navbar } from '@/components/Navbar'; +import { useApp } from '@/hooks/useApp'; + +/** + * A wrapper component to bridge the global AppContext with the Navbar. + * This ensures the Navbar can access the API key and state from anywhere in the app. + */ +export const NavbarWrapper: React.FC = () => { + const { apiKey, setApiKey } = useApp(); + + return ; +}; diff --git a/src/components/TripHistory.test.tsx b/src/components/TripHistory.test.tsx new file mode 100644 index 0000000..8d2f6b5 --- /dev/null +++ b/src/components/TripHistory.test.tsx @@ -0,0 +1,58 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import '@testing-library/jest-dom'; +import { TripHistory } from './TripHistory'; +import { Trip } from '@/types/trip'; + +const mockTrips: Trip[] = [ + { + id: '1', + title: 'Trip 1', + createdAt: '2026-06-16', + prompt: 'Prompt 1', + content: 'Content 1', + }, + { + id: '2', + title: 'Trip 2', + createdAt: '2026-06-17', + prompt: 'Prompt 2', + content: 'Content 2', + }, +]; + +describe('TripHistory', () => { + it('renders a list of trips', () => { + render( + {}} onDelete={() => {}} />, + ); + expect(screen.getByText('Trip 1')).toBeInTheDocument(); + expect(screen.getByText('Trip 2')).toBeInTheDocument(); + }); + + it('calls onSelect when a trip is clicked', () => { + const handleSelect = vi.fn(); + render( + {}} />, + ); + fireEvent.click(screen.getByText('Trip 2')); + expect(handleSelect).toHaveBeenCalledWith(1); + }); + + it('calls onDelete when the delete button is clicked', () => { + const handleDelete = vi.fn(); + render( + {}} onDelete={handleDelete} />, + ); + + // The delete button is in the list item + const deleteButtons = screen.getAllByTitle(/Delete trip/i); + fireEvent.click(deleteButtons[0]); + expect(handleDelete).toHaveBeenCalledWith('1'); + }); + + it('shows empty state when no trips', () => { + render( {}} onDelete={() => {}} />); + expect(screen.getByText(/No trips planned yet/i)).toBeInTheDocument(); + }); +}); diff --git a/src/components/TripHistory.tsx b/src/components/TripHistory.tsx new file mode 100644 index 0000000..a6d33ab --- /dev/null +++ b/src/components/TripHistory.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { IoTimeOutline, IoTrashOutline } from 'react-icons/io5'; +import { Trip } from '@/types/trip'; + +interface TripHistoryProps { + trips: Trip[]; + activeIndex: number; + onSelect: (index: number) => void; + onDelete: (id: string) => void; +} + +export const TripHistory: React.FC = ({ + trips, + activeIndex, + onSelect, + onDelete, +}) => { + if (trips.length === 0) { + return
No trips planned yet.
; + } + + return ( +
+

+ Trip History +

+
+ {trips.map((trip, index) => ( +
  • + + +
  • + ))} +
    +
    + ); +}; diff --git a/src/components/WorkspaceActions.test.tsx b/src/components/WorkspaceActions.test.tsx index c71793f..3a88083 100644 --- a/src/components/WorkspaceActions.test.tsx +++ b/src/components/WorkspaceActions.test.tsx @@ -13,9 +13,9 @@ describe('WorkspaceActions', () => { onShare={() => {}} />, ); - expect(screen.getByText(/Serverless Workspace/i)).toBeInTheDocument(); + expect(screen.getByText(/Local Workspace/i)).toBeInTheDocument(); expect(screen.getByRole('button', { name: /Export/i })).toBeDisabled(); - expect(screen.getByRole('button', { name: /Share Active Link/i })).toBeDisabled(); + expect(screen.getByRole('button', { name: /Share Active Trip/i })).toBeDisabled(); }); it('enables buttons when totalTrips > 0', () => { @@ -28,7 +28,7 @@ describe('WorkspaceActions', () => { />, ); expect(screen.getByRole('button', { name: /Export/i })).not.toBeDisabled(); - expect(screen.getByRole('button', { name: /Share Active Link/i })).not.toBeDisabled(); + expect(screen.getByRole('button', { name: /Share Active Trip/i })).not.toBeDisabled(); }); it('calls onExport when Export button is clicked', () => { @@ -55,7 +55,7 @@ describe('WorkspaceActions', () => { onShare={handleShare} />, ); - fireEvent.click(screen.getByRole('button', { name: /Share Active Link/i })); + fireEvent.click(screen.getByRole('button', { name: /Share Active Trip/i })); expect(handleShare).toHaveBeenCalled(); }); diff --git a/src/components/WorkspaceActions.tsx b/src/components/WorkspaceActions.tsx index 1109a03..b440dda 100644 --- a/src/components/WorkspaceActions.tsx +++ b/src/components/WorkspaceActions.tsx @@ -11,32 +11,36 @@ interface WorkspaceActionsProps { export const WorkspaceActions: React.FC = React.memo( ({ totalTrips, onExport, onImport, onShare }) => { return ( -
    -
    -

    - Serverless Workspace +
    +
    +

    + Local Workspace

    -
    -
    +
    ); }, diff --git a/src/hooks/useApp.tsx b/src/hooks/useApp.tsx new file mode 100644 index 0000000..fb124cd --- /dev/null +++ b/src/hooks/useApp.tsx @@ -0,0 +1,50 @@ +'use client'; + +import React, { createContext, useContext, useState, useCallback } from 'react'; +import { Trip } from '@/types/trip'; +import { useTrips } from '@/hooks/useTrips'; + +interface AppContextType { + apiKey: string; + setApiKey: (key: string) => void; + trips: Trip[]; + activeTrip: Trip | null; + activeIndex: number; + setActiveIndex: (index: number) => void; + addTrips: (trips: Trip[]) => void; + deleteTrip: (id: string) => void; + totalTrips: number; + isLoaded: boolean; +} + +const AppContext = createContext(undefined); + +export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const tripHelpers = useTrips(); + const [apiKey, setInternalApiKey] = useState(() => { + if (typeof window !== 'undefined') { + return localStorage.getItem('hf_api_key') || ''; + } + return ''; + }); + + const setApiKey = useCallback((key: string) => { + const trimmedKey = key.trim(); + setInternalApiKey(trimmedKey); + localStorage.setItem('hf_api_key', trimmedKey); + }, []); + + return ( + + {children} + + ); +}; + +export const useApp = () => { + const context = useContext(AppContext); + if (context === undefined) { + throw new Error('useApp must be used within an AppProvider'); + } + return context; +}; diff --git a/src/services/ai.ts b/src/services/ai.ts index ddc5848..9cdecce 100644 --- a/src/services/ai.ts +++ b/src/services/ai.ts @@ -11,29 +11,36 @@ export interface GenerationOptions { } const SYSTEM_PROMPT = ` -You are a travel itinerary expert. Transform user notes into a structured JSON object. -TONE: Factual, concise, and informative. No poetic or romanced language. +You are a "Point-to-Point" trip planning assistant. +Your primary task is to extract departure and arrival locations and format the user's description. + +CORE PHILOSOPHY: +- Plan trips one-by-one. +- The arrival point of one trip is often the starting point for the next. +- NO AI-generated route advice. Google Maps handles all navigation logic. RULES: 1. Output MUST be valid JSON. -2. Language: Use the SAME language as the user's request for all content and headers. -3. Headers: Use '###' (Heading 3) for all section titles. -4. Title: Follow the pattern "[Destination]: [Catchphrase]" (e.g., "Tsuwano: Charme et authenticité"). -5. Geocoding: For the "start" and "stop" fields, provide the full geographic name (e.g., "City, State/Province, Country"). -6. Maps: At the VERY BEGINNING of the 'content' field, provide a standard markdown link to the Google Maps directions. - The link name MUST be the localized version of "Open itinerary in Google Maps" (e.g., "Ouvrir l'itinéraire dans Google Maps"). - URL Format: Use '+' instead of spaces/commas in the URL path. - Example: [Ouvrir l'itinéraire dans Google Maps](https://www.google.com/maps/dir/City+State+Country/City+State+Country/) - This link MUST be the first line of the content. -7. Logistics: List start and end points below the map link. -8. No Title: Do NOT include the trip title inside the 'content' markdown. +2. Language: Use the SAME language as the user's request. +3. Extraction: + - Identify "start" (Departure) and "stop" (Arrival). + - Use full geographic names (e.g., "Paris, France"). +4. Description: + - Strictly follow the user's input for the arrival description. + - ONLY perform grammatical/syntax corrections and apply clean Markdown (bullets, bolding). + - Do NOT add new information or suggestions. +5. Maps Link: + - The FIRST line of 'content' MUST be a Google Maps directions link. + - Format: [Localized "Open Routes in Google Maps"](https://www.google.com/maps/dir/Start+Location/Stop+Location/) + - Use '+' instead of spaces/commas in the URL. +6. Title: "[Start] → [Arrival]". JSON SCHEMA: { - "title": "Destination: Catchphrase", - "start": "City, State, Country", - "stop": "City, State, Country", - "content": "[Ouvrir l'itinéraire dans Google Maps](URL)\\n\\n### Section Name\\n- **Data**: Value" + "title": "Start → Arrival", + "start": "City, Country", + "stop": "City, Country", + "content": "[Open Routes in Google Maps](URL)\\n\\n### Arrival: [Arrival Name]\\n[Formatted User Notes]" } `.trim(); diff --git a/src/utils/analytics.ts b/src/utils/analytics.ts new file mode 100644 index 0000000..0e68e2e --- /dev/null +++ b/src/utils/analytics.ts @@ -0,0 +1,30 @@ +/** + * Utility to track custom events with Umami Analytics. + * Events are only tracked if the Umami script is loaded. + */ + +interface Umami { + track: (eventName: string, eventData?: Record) => void; +} + +declare global { + interface Window { + umami?: Umami; + } +} + +export const trackEvent = ( + eventName: string, + eventData?: Record, +) => { + if (typeof window !== 'undefined' && window.umami) { + window.umami.track(eventName, eventData); + } +}; + +export const ANALYTICS_EVENTS = { + TRIP_PLANNED: 'trip_planned', + TRIP_SHARED: 'trip_shared', + TRIP_EXPORTED: 'trip_exported', + TRIP_DELETED: 'trip_deleted', +}; From 3903da5bb84435ad16834b2c361921e664e3fb64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Hameau?= Date: Tue, 16 Jun 2026 23:51:13 +0200 Subject: [PATCH 02/20] feat: refine rigid workspace layout, restore api key visibility, and optimize overscroll behavior --- specs/pending/ui-refinement-v1.md | 50 ++++++++++++++++++++ src/app/globals.css | 48 ++++++++++++++----- src/app/layout.tsx | 78 ++++++++----------------------- src/app/page.tsx | 24 +++++----- src/components/GenerationForm.tsx | 28 +++++++---- src/components/Navbar.tsx | 6 +-- src/components/TripHistory.tsx | 27 ++++++----- 7 files changed, 157 insertions(+), 104 deletions(-) create mode 100644 specs/pending/ui-refinement-v1.md diff --git a/specs/pending/ui-refinement-v1.md b/specs/pending/ui-refinement-v1.md new file mode 100644 index 0000000..b442ef8 --- /dev/null +++ b/specs/pending/ui-refinement-v1.md @@ -0,0 +1,50 @@ +# Spec: UI/UX Refinement & "Point-to-Point" Polish (v1) + +This specification addresses the identified flaws in the current "Point-to-Point" implementation, focusing on workflow efficiency, mobile usability, and professional UI standards. + +## 1. User-Identified Flaws + +- **Missing API Key Input**: The API key input has disappeared from the UI, preventing users from setting up their credentials. +- **Buggy Sidebar**: The left panel doesn't reflow content correctly when text is too long (causing overflow/cutoff) and wastes horizontal space. +- **Scrollbar & Footer Issues**: Multiple nested scrollbars are present, and the footer is in an "unknown status" (buggy positioning/scrolling). +- **AI Prompt Concerns**: User has potential concerns with the current prompt; agent MUST ask questions for feedback after user testing. + +## 2. Agent-Identified Refinements + +### 2.1 Workspace Flow & Layout + +- **Mobile Sidebar**: Refactor `TripHistory` into a `drawer` (off-canvas) or move it below the main form to ensure the `GenerationForm` is immediately accessible. +- **State Persistence**: Ensure the `AppProvider` handles hydration and local storage synchronization flawlessly across all pages (`/`, `/about`, `/how-to`). + +### 2.2 Functional Precision (Point-to-Point) + +- **Explicit Inputs**: Refactor `GenerationForm` to include distinct fields for "Start" and "Arrival" to guarantee 100% Google Maps accuracy without relying on AI extraction. +- **Leg Chaining**: Add a "Plan Next Leg" button to the `TripViewer`. Clicking this will pre-fill the "Start" field of the next trip with the current "Arrival". + +### 2.3 Aesthetics & Interaction + +- **Transitions**: Implement smooth transitions (e.g., using CSS transitions or Framer Motion) when switching between trips in the history. +- **Visual Hierarchy**: Refine the `TripHistory` sidebar with better active states and hover effects. + +## 3. Implementation Plan + +### Phase 1: User Flaws & Input Refactoring + +- [ ] Address the list of flaws provided by the user. +- [ ] Refactor `GenerationForm` with explicit "Start" and "Arrival" fields. + +### Phase 2: Workflow & Layout + +- [ ] Implement the "Plan Next Leg" feature. +- [ ] Fix the mobile sidebar/drawer layout issue. + +### Phase 3: Polish & Validation + +- [ ] Apply aesthetic refinements and transitions. +- [ ] Run full `npm run check` and update tests. + +## 4. Success Criteria + +- [ ] All user-identified flaws are resolved. +- [ ] The "Point-to-Point" workflow feels intuitive and robust. +- [ ] The app is highly usable on both desktop and mobile. diff --git a/src/app/globals.css b/src/app/globals.css index cb335a6..89f004a 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,18 +2,44 @@ @plugin "daisyui"; @layer base { - html { - scroll-behavior: smooth; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; + html, + body { + height: 100%; + margin: 0; + padding: 0; } } -/* In v4, keep your plugin overrides in standard utility utility scopes - if you want to avoid strict build-time reference check crashes. -*/ -body { - background-color: var(--bg-base-200, #f2f2f2); - color: var(--fallback-bc, initial); - min-height: 100vh; +/* Rigid Application Frame */ +.app-frame { + display: flex; + flex-direction: column; + height: 100vh; + width: 100vw; + overflow: hidden; +} + +/* Workspace: Sidebar + Content Area */ +.app-workspace { + display: flex; + flex: 1; + overflow: hidden; +} + +/* Locked Sidebar */ +.app-sidebar { + flex: none; + display: flex; + flex-direction: column; + background-color: var(--bg-base-100); + border-right: 1px solid var(--border-base-200); + overflow: hidden; +} + +/* Elastic Content Area */ +.app-content { + flex: 1; + overflow-y: auto; + overscroll-behavior-y: auto; + background-color: color-mix(in srgb, var(--bg-base-200), transparent 70%); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 1debf01..cc8293b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,88 +2,50 @@ import type { Metadata, Viewport } from 'next'; import { Inter } from 'next/font/google'; import './globals.css'; -// Optimize typography loading directly from Google Fonts via Next.js const inter = Inter({ subsets: ['latin'], variable: '--font-inter', display: 'swap', }); -// Production SEO metadata architecture export const metadata: Metadata = { metadataBase: new URL('https://gehdoc.github.io/quick-tripper/'), title: 'Quick-tripper — Private Point-to-Point AI Trip Planner', description: - 'Plan your journeys one-by-one with privacy-first AI. Free, serverless, and zero-tracking. Bring your own Hugging Face key.', - keywords: [ - 'Private AI Trip Planner', - 'Point-to-Point Route Planner', - 'Zero-Backend Travel App', - 'Hugging Face Travel AI', - 'MIT Licensed Trip Planner', - 'Serverless Road Trip', - ], - authors: [{ name: 'Quick-tripper Contributors' }], - openGraph: { - title: 'Quick-tripper — Private AI Trip Planner', - description: 'Privacy-first, zero-backend point-to-point trip planning.', - url: 'https://gehdoc.github.io/quick-tripper/', - siteName: 'Quick-tripper', - images: [ - { - url: 'icon.svg', - width: 800, - height: 600, - }, - ], - locale: 'en_US', - type: 'website', - }, - twitter: { - card: 'summary_large_image', - title: 'Quick-tripper — Private AI Trip Planner', - description: 'Privacy-first, zero-backend point-to-point trip planning.', - images: ['icon.svg'], - }, - robots: 'index, follow', - icons: { - icon: 'icon.svg', - apple: 'icon.svg', - }, + 'Plan your journeys one-by-one with privacy-first AI. Free, serverless, and zero-tracking.', + keywords: ['Private AI Trip Planner', 'Point-to-Point Route Planner', 'Serverless Road Trip'], + icons: { icon: 'icon.svg', apple: 'icon.svg' }, }; -// Explicit viewport rules to prevent layout scaling breaking on mobile devices export const viewport: Viewport = { - themeColor: '#4f46e5', // Matches your primary branding color + themeColor: '#4f46e5', width: 'device-width', initialScale: 1, - maximumScale: 1, }; -import { Footer } from '@/components/Footer'; import { NavbarWrapper } from '@/components/NavbarWrapper'; import { AppProvider } from '@/hooks/useApp'; -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { +export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - // 'data-theme' tells DaisyUI which default theme colors to map to your classes - + - + {process.env.NODE_ENV === 'production' && ( + + )} - + - -
    {children}
    -