# Project export: Trekly

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2025
- Tagline: We've all been there, you have an awesome project idea, a new adventure you'd like to take on, tons of resources bookmarked, and genuine excitement to start—but somehow, you never do. Let's fix that.
- Devpost: https://devpost.com/software/trekly
- GitHub: https://github.com/khankamolk/Trekly
- Team: 2 GitHub contributor(s) — Khankamol Chor Kongrukgreatiyos (15 commits), Hannah Zhuang (12 commits)

## Devpost submission (written by the team)

### Inspiration

We’ve all had ideas we were excited about—starting a blog, building an app, launching a startup—but most of them never make it past the first day. The problem isn’t a lack of motivation; it’s a lack of direction. When faced with too many resources and no clear path forward, people procrastinate or give up entirely. We wanted to solved that.

### What it does

Trekly is a personal AI project planner that transforms vague ideas into structured, step-by-step roadmaps tailored to your goals, timeline, skill level, and learning preferences. It generates a week-by-week plan filled with actionable steps, interactive activities, and curated resources. Each action plan is designed not just to teach a skill, but to help you build confidence, develop momentum, and reflect on your growth. Trekly is about more than just finishing a project—it’s about accomplishing something you never thought you could. Every step is designed to move you closer to a better, more capable version of yourself. By showing up consistently, learning actively, and pushing through uncertainty, Trekly helps you build not just skills, but belief in your own potential.

### How we built it

Used React frontend and JavaScript to create a smooth, responsive onboarding and roadmap experience. Project details questionnaire inputs are processed through Google Gemini and refined with a carefully engineered prompt, which generates a structured, JSON-based roadmap. Chatbot interface powered by the Claude API (Anthropic), which understands the user's current progress and context to provide live feedback, encouragement, and step-by-step support.

### What we learned

Clear, structured instructions can dramatically improve the quality and reliability of LLM-generated outputs. We also realized how important it is to deeply consider the user’s motivation and what they hope to accomplish when using the app. Building Trekly taught us how to balance structure with flexibility, giving users enough guidance without restricting creativity.

### What's next

Next, we want to let users save and share their roadmaps, track progress step-by-step, and attach links, images, or code as they build. We’re also exploring more domain-specific templates, like “launching a portfolio,” “learning full-stack development,” or “preparing for technical interviews.” Our goal is to turn Trekly into a personal project coach—one that not only helps you get started, but keeps you going, celebrates your progress, and grows with you.

## README (from the GitHub repository)

# Trekly

## Setting Up

Create `.env` from template file.
```
cp .env.template .env
```
Set up local code editor intellisense.
```
npm install
```
## Running the project locally
Run the following command.
```
npm run dev
```


## Detected evidence (automated analysis)

Indexed codebase: 13 recognized source files, 78 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (18 of 18)

```
.DS_Store
.env.template
.gitignore
eslint.config.js
index.html
package.json
README.md
src/api/generateRoadmap.js
src/App.jsx
src/data.json
src/index.css
src/main.jsx
src/pages/Onboarding.jsx
src/pages/Roadmap.jsx
src/style/RoadMap.css
src/styles/Onboarding.css
src/styles/Roadmap.css
vite.config.js
```

### Dependencies

- package.json: @anthropic-ai/sdk@^0.54.0, @eslint/js@^9.25.0, @google/genai@^1.6.0, @tailwindcss/vite@^4.1.10, @types/react@^19.1.2, @types/react-dom@^19.1.2, @vitejs/plugin-react@^4.4.1, autoprefixer@^10.4.21, eslint@^9.25.0, eslint-plugin-react-hooks@^5.2.0, eslint-plugin-react-refresh@^0.4.19, formidable@^3.5.4, globals@^16.0.0, lucide-react@^0.522.0, postcss@^8.5.6, react@^19.1.0, react-dom@^19.1.0, react-router-dom@^7.6.2, tailwindcss@^4.1.10, vite@^6.3.5

### Recent commits (newest first)

- Final
- fixing merges
- Added more error handling for loader
- Added more error handling for loader
- fixing merges
- pushing semi final edits
- Finished the activities
- Merge branch 'main' of https://github.com/khankamolk/Trekly
- Han's version before icon changes
- Fixed step numbers
- Fixing some UI
- Remove secret from code
- Fixing Merges
- fixing  merges
- Merged
- changed some style
- Finetuning
- Finetuning
- resolving merges
- First Version Roadmap

## Key source files (fetched from GitHub, selected and truncated for size)

### package.json

```
{
  "name": "my-app",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "@anthropic-ai/sdk": "^0.54.0",
    "@google/genai": "^1.6.0",
    "@tailwindcss/vite": "^4.1.10",
    "formidable": "^3.5.4",
    "lucide-react": "^0.522.0",
    "react": "^19.1.0",
    "react-dom": "^19.1.0"
  },
  "devDependencies": {
    "@eslint/js": "^9.25.0",
    "@types/react": "^19.1.2",
    "@types/react-dom": "^19.1.2",
    "@vitejs/plugin-react": "^4.4.1",
    "autoprefixer": "^10.4.21",
    "eslint": "^9.25.0",
    "eslint-plugin-react-hooks": "^5.2.0",
    "eslint-plugin-react-refresh": "^0.4.19",
    "globals": "^16.0.0",
    "postcss": "^8.5.6",
    "react-router-dom": "^7.6.2",
    "tailwindcss": "^4.1.10",
    "vite": "^6.3.5"
  }
}

```

### src/main.jsx

```javascript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

```

### src/App.jsx

```javascript
import ReactDOM from "react-dom/client";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { useState } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import {PackagePlus, Folder} from 'lucide-react';

import Onboarding from './pages/Onboarding.jsx'
import Roadmap from "./pages/Roadmap.jsx";

// Main App Component
function AppContent() {
  return (
    <>
      <Routes>
        <Route path="/" element={<Onboarding />} />
        <Route path="/roadmap" element={<Roadmap />} />
      </Routes>
    </>
  );
}

export default function App() {
  return (
    <BrowserRouter>
      <AppContent />
    </BrowserRouter>
  );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
```

### vite.config.js

```javascript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'

// https://vite.dev/config/
export default defineConfig({
  plugins: [react(), tailwindcss()],
})

```

### index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/src/assets/chatpal.png" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Trekly</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

```

### eslint.config.js

```javascript
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
export default [
  { ignores: ['dist'] },
  {
    files: ['**/*.{js,jsx}'],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
      parserOptions: {
        ecmaVersion: 'latest',
        ecmaFeatures: { jsx: true },
        sourceType: 'module',
      },
    },
    plugins: {
      'react-hooks': reactHooks,
      'react-refresh': reactRefresh,
    },
    rules: {
      ...js.configs.recommended.rules,
      ...reactHooks.configs.recommended.rules,
      'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
      'react-refresh/only-export-components': [
        'warn',
        { allowConstantExport: true },
      ],
    },
  },
]

```

### src/index.css

```css
@import "tailwindcss";

:root {
  font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
  line-height: 1.5;
  font-weight: 400;

  color-scheme: light dark;
  color: rgba(255, 255, 255, 0.87);
  background-color: #242424;

  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

a {
  font-weight: 500;
  color: #646cff;
  text-decoration: inherit;
}
a:hover {
  color: #535bf2;
}

body {
  margin: 0;
  display: flex;
  place-items: center;
  min-width: 320px;
  min-height: 100vh;
}

h1 {
  font-size: 3.2em;
  line-height: 1.1;
}

button {
  border-radius: 8px;
  border: 1px solid transparent;
  padding: 0.6em 1.2em;
  font-size: 1em;
  font-weight: 500;
  font-family: inherit;
  background-color: #1a1a1a;
  cursor: pointer;
  transition: border-color 0.25s;
}
button:hover {
  border-color: #646cff;
}
button:focus,
button:focus-visible {
  outline: 4px auto -webkit-focus-ring-color;
}

@media (prefers-color-scheme: light) {
  :root {
    color: #213547;
    background-color: #ffffff;
  }
  a:hover {
    color: #747bff;
  }
  button {
    background-color: #f9f9f9;
  }
}

```

### src/style/RoadMap.css

```css
@import "tailwindcss";

#root {
  max-width: 1280px;
  margin: 0 auto;
  padding: 2rem;
  text-align: center;
}

.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
  transition: filter 300ms;
}
.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
  filter: drop-shadow(0 0 2em #61dafbaa);
}

@keyframes logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

@media (prefers-reduced-motion: no-preference) {
  a:nth-of-type(2) .logo {
    animation: logo-spin infinite 20s linear;
  }
}

.card {
  padding: 2em;
}

.read-the-docs {
  color: #888;
}

```

### src/api/generateRoadmap.js

```javascript
import { GoogleGenAI } from '@google/genai';

class APIKeyError extends Error {
    constructor(message) {
        super(message);
        this.name = 'APIKeyError';
    }
}

class JSONParseError extends Error {
    constructor(message, rawResponse) {
        super(message);
        this.name = 'JSONParseError';
        this.rawResponse = rawResponse;
    }
}

class ValidationError extends Error {
    constructor(message, missingFields) {
        super(message);
        this.name = 'ValidationError';
        this.missingFields = missingFields;
    }
}


// Validate the generated roadmap structure
function validateRoadmapStructure(roadmap) {
    const requiredProps = ['title', 'totalDuration', 'difficulty', 'avatar', 'worlds'];
    const missingFields = [];
    
    for (const prop of requiredProps) {
        if (!roadmap[prop]) {
            missingFields.push(prop);
        }
    }
    
    if (missingFields.length > 0) {
        throw new ValidationError(
            `Roadmap is missing required fields: ${missingFields.join(', ')}`,
            missingFields
        );
    }
    
    if (!Array.isArray(roadmap.worlds) || roadmap.worlds.length === 0) {
        throw new ValidationError('Worlds must be a non-empty array', ['worlds']);
    }
    
    // Validate each world has required properties
    roadmap.worlds.forEach((world, index) => {
        const worldRequiredProps = ['worldId', 'title', 'description', 'steppingStones'];
        const worldMissingFields = [];
        
        worldRequiredProps.forEach(prop => {
            if (!world[prop]) {
                worldMissingFields.push(`world[${index}].${prop}`);
            }
        });
        
        if (worldMissingFields.length > 0) {
            throw new ValidationError(
                `World ${index + 1} is missing required fields: ${worldMissingFields.join(', ')}`,
                worldMissingFields
            );
        }
        
        if (!Array.isArray(world.steppingStones) || world.steppingStones.length === 0) {
            throw new ValidationError(
                `World ${index + 1} must have at least one stepping stone`,
                [`world[${index}].steppingStones`]
            );
        }
    });
}

// Clean and parse JSON response
function cleanAndParseJSON(jsonStr) {
    // Remove markdown code blocks if present
    let cleaned = jsonStr.trim();
    const codeBlockMatch = cleaned.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/);
    if (codeBlockMatch) {
        cleaned = codeBlockMatch[1].trim();
    }
    
    // Remove any leading/trailing non-JSON content
    const startIndex = cleaned.indexOf('{');
    const endIndex = cleaned.lastIndexOf('}');
    
    if (startIndex === -1 || endIndex === -1 || startIndex >= endIndex) {
        throw new JSONParseError('No valid JSON object found in response', cleaned);
    }
    
    cleaned = cleaned.substring(startIndex, endIndex + 1);
    
    try {
        const parsed = JSON.parse(cleaned);
        validateRoadmapStructure(parsed);
        return parsed;
    } catch (error) {
        if (error instanceof SyntaxError) {
            throw new JSONParseError(`Invalid JSON syntax: ${error.message}`, cleaned);
        }
        throw error;
    }
}

// Enhanced generateRoadmap function with retry logic
export async function generateRoadmap(formData, onProgress = null) {
    const MAX_RETRIES = 3;
    const RETRY_DELAY = 1000; 
    
    const apiKey = import.meta.env.VITE_GEMINI_API_KEY;
    if (!apiKey) {
        throw new APIKeyError('GEMINI API key is missing. Please check your environment configuration.');
    }
    
    const ai = new GoogleGenAI({ apiKey });
    
    const prompt = `
        System Prompt:
        You are an expert project planning assistant. Your task is to generate a comprehensive, step-by-step project roadmap based on user-provided details. The output MUST be a valid JSON object adhering to the schema provided below. Do not include any explanatory text, comments, or markdown formatting like \`\`\`json around the JSON object itself.

        CRITICAL: Your response must be ONLY a valid JSON object that can be parsed by JSON.parse(). 

        JSON FORMATTING REQUIREMENTS:
        - Start your response with { and end with }
        - Use double quotes (") for all strings, never single quotes (')
        - Ensure all brackets [ ] and braces { } are properly matched
        - Include commas after every property except the last one in each object/array
        - Do not include trailing commas
        - Do not include any text before or after the JSON object
        - Do not use markdown formatting like \`\`\`json
        - Escape any quotes within strings using \"

        RESOURCES REQUIREMENTS:
        - Each step should include 2-3 helpful learning tips in the "resources" array
        - Tips should be practical, actionable advice specific to the step's content
        - Include specific search suggestions (e.g., "YouTube Search: 'React hooks tutorial 2024'")
        - Include learning strategies and best practices
        - Include common pitfalls to avoid
        - Include tools or techniques that would help with this specific step
        - Make each tip concise but valuable (1-2 sentences max)
        - DO NOT include actual URLs or links - only helpful guidance and search suggestions

        User-Provided Details:
        - Project Goal: ${formData.projectGoal}
        - Initial Ideas/Resources/Blockers: ${formData.initialIdeas || 'Not specified'}
        - Project Start Date: ${formData.startDate || 'Not specified'} 
        - Project End Date: ${formData.endDate || 'Not specified'}
        - Time Commitment: ${formData.timeCommitment || 'Not specified'}
        - Experience Level: ${formData.experienceLevel}
        - Current Skills/Tools: ${formData.currentSkills || 'Not specified'}
        - Learning Goals from Project: ${formData.learningGoals || 'Not specified'}
        - Desired Project Name: ${formData.projectName} 
        - A
[truncated — 6872 more characters]
```

### src/pages/Onboarding.jsx

```javascript
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { generateRoadmap } from '../api/generateRoadmap';
import { X, ChevronRight, ChevronLeft, Target, Calendar, User, BookOpen, Sparkles, Clock, Award, Brain } from 'lucide-react';

export default function Onboarding() {
  const [formData, setFormData] = useState({
    projectGoal: '',
    initialIdeas: '',
    startDate: '',
    endDate: '',
    timeCommitment: '',
    experienceLevel: '',
    currentSkills: '',
    learningGoals: '',
    projectName: '',
    autoGenerateName: '',
  });

  const [step, setStep] = useState(0);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);
  const navigate = useNavigate();

  const totalSteps = 4;

  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    if (type === 'checkbox') {
      setFormData({ 
        ...formData, 
        [name]: checked,
        ...(name === 'autoGenerateName' && checked && { projectName: '' })
      });
    } else {
      setFormData({ ...formData, [name]: value });
    }
  };

  const handleNext = () => setStep((prev) => prev + 1);
  const handleBack = () => setStep((prev) => prev - 1);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsLoading(true);
    setError(null);

    try {
      const roadmap = await generateRoadmap(formData);
      navigate('/roadmap', { state: { roadmap } });
    } catch (err) {
      setError(err.message);
    } finally {
      setIsLoading(false);
    }
  };

  const stepConfig = [
    {
      title: "Define Your Vision",
      subtitle: "Tell us about what you want to create",
      icon: Target,
      color: "from-violet-500 to-purple-600"
    },
    {
      title: "Timeline & Commitment",
      subtitle: "Set your timeline and availability for this project",
      icon: Calendar,
      color: "from-blue-500 to-indigo-600"
    },
    {
      title: "Your Experience",
      subtitle: "Help us understand your current skill level",
      icon: User,
      color: "from-emerald-500 to-teal-600"
    },
    {
      title: "Learning Preferences",
      subtitle: "Customize your learning journey and project details",
      icon: BookOpen,
      color: "from-orange-500 to-red-600"
    }
  ];

  const steps = [
    // Step 1: Project Vision
    <div className="space-y-6" key="step1">
      <div className="space-y-4">
        <div className="relative">
          <Target className="absolute left-3 top-3 h-5 w-5 text-violet-500" />
          <input 
            name="projectGoal" 
            placeholder="What do you want to create? (e.g., launch a zine, build a game, start a podcast)"
            value={formData.projectGoal} 
            onChange={handleChange} 
            className="w-full pl-12 pr-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-violet-500 focus:border-transparent transition-all duration-200 bg-white/80 backdrop-blur-sm" 
          />
        </div>
        <div className="relative">
          <Brain className="absolute left-3 top-3 h-5 w-5 text-violet-500" />
          <textarea 
            name="initialIdeas" 
            placeholder="Got an idea already brewing? Any blockers? Tell us more..." 
            value={formData.initialIdeas} 
            onChange={handleChange} 
            rows={4}
            className="w-full pl-12 pr-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-violet-500 focus:border-transparent transition-all duration-200 bg-white/80 backdrop-blur-sm resize-none" 
          />
        </div>
      </div>
    </div>,

    // Step 2: Timeline
    <div className="space-y-6" key="step2">
      <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
        <div className="space-y-2">
          <label className="text-sm font-medium text-gray-700">Start Date</label>
          <div className="relative">
            <Calendar className="absolute left-3 top-3 h-5 w-5 text-blue-500" />
            <input 
              name="startDate" 
              type="date" 
              value={formData.startDate} 
              onChange={handleChange} 
              className="w-full pl-12 pr-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 bg-white/80 backdrop-blur-sm" 
            />
          </div>
        </div>
        <div className="space-y-2">
          <label className="text-sm font-medium text-gray-700">Target End Date</label>
          <div className="relative">
            <Calendar className="absolute left-3 top-3 h-5 w-5 text-blue-500" />
            <input 
              name="endDate" 
              type="date" 
              value={formData.endDate} 
              onChange={handleChange} 
              className="w-full pl-12 pr-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 bg-white/80 backdrop-blur-sm" 
            />
          </div>
        </div>
      </div>
      <div className="relative">
        <Clock className="absolute left-3 top-3 h-5 w-5 text-blue-500" />
        <select 
          name="timeCommitment" 
          value={formData.timeCommitment} 
          onChange={handleChange} 
          className="w-full pl-12 pr-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 bg-white/80 backdrop-blur-sm appearance-none cursor-pointer"
        >
          <option value="">How much time can you commit per week?</option>
          <option value="1-3 hours">1-3 hours per week</option>
          <option value="4-8 hours">4-8 hours per week</option>
          <option value="9-15 hours">9-15 hours per week</option>
          <option value="16+ hours">16+ hours per week</option>
        </select>
      </div>
    </div>,

    // Step 3: Experience
    <div className="space-y-6" key="step3">
      <div className="space-y-4"
[truncated — 11292 more characters]
```

[2 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]