Skip to content
Open
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
64 changes: 64 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Claude Code Task Management Guide

## Documentation Available

📚 **Project Documentation**: Check the documentation files in this directory for project-specific setup instructions and guides.
**Project Tasks**: Check the tasks directory in documentation/tasks for the list of tasks to be completed. Use the CLI commands below to interact with them.

## MANDATORY Task Management Workflow

🚨 **YOU MUST FOLLOW THIS EXACT WORKFLOW - NO EXCEPTIONS** 🚨

### **STEP 1: DISCOVER TASKS (MANDATORY)**
You MUST start by running this command to see all available tasks:
```bash
task-manager list-tasks
```

### **STEP 2: START EACH TASK (MANDATORY)**
Before working on any task, you MUST mark it as started:
```bash
task-manager start-task <task_id>
```

### **STEP 3: COMPLETE OR CANCEL EACH TASK (MANDATORY)**
After finishing implementation, you MUST mark the task as completed, or cancel if you cannot complete it:
```bash
task-manager complete-task <task_id> "Brief description of what was implemented"
# or
task-manager cancel-task <task_id> "Reason for cancellation"
```

## Task Files Location

📁 **Task Data**: Your tasks are organized in the `documentation/tasks/` directory:
- Task JSON files contain complete task information
- Use ONLY the `task-manager` commands listed above
- Follow the mandatory workflow sequence for each task

## MANDATORY Task Workflow Sequence

🔄 **For EACH individual task, you MUST follow this sequence:**

1. 📋 **DISCOVER**: `task-manager list-tasks` (first time only)
2. 🚀 **START**: `task-manager start-task <task_id>` (mark as in progress)
3. 💻 **IMPLEMENT**: Do the actual coding/implementation work
4. ✅ **COMPLETE**: `task-manager complete-task <task_id> "What was done"` (or cancel with `task-manager cancel-task <task_id> "Reason"`)
5. 🔁 **REPEAT**: Go to next task (start from step 2)

## Task Status Options

- `pending` - Ready to work on
- `in_progress` - Currently being worked on
- `completed` - Successfully finished
- `blocked` - Cannot proceed (waiting for dependencies)
- `cancelled` - No longer needed

## CRITICAL WORKFLOW RULES

❌ **NEVER skip** the `task-manager start-task` command
❌ **NEVER skip** the `task-manager complete-task` command (use `task-manager cancel-task` if a task is not planned, not required, or you must stop it)
❌ **NEVER work on multiple tasks simultaneously**
✅ **ALWAYS complete one task fully before starting the next**
✅ **ALWAYS provide completion details in the complete command**
✅ **ALWAYS follow the exact 3-step sequence: list → start → complete (or cancel if not required)**
113 changes: 113 additions & 0 deletions app/api/contact/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'

const contactSchema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Please enter a valid email address'),
company: z.string().optional(),
phone: z.string().optional(),
message: z.string().min(10, 'Message must be at least 10 characters'),
budget: z.enum(['5k-15k', '15k-50k', '50k-100k', '100k+', 'not-sure']).optional(),
})

export async function POST(request: NextRequest) {
try {
const body = await request.json()

// Basic rate limiting using timestamp (in production, use a proper rate limiter)
const forwarded = request.headers.get('x-forwarded-for')
const ip = forwarded ? forwarded.split(',')[0] : request.headers.get('x-real-ip') || 'unknown'

// Validate the form data
const validatedData = contactSchema.parse(body)

// Basic spam protection - check for obvious spam patterns
const spamKeywords = ['viagra', 'casino', 'porn', 'lottery', 'winner']
const messageContent = `${validatedData.name} ${validatedData.message}`.toLowerCase()
const hasSpam = spamKeywords.some(keyword => messageContent.includes(keyword))

if (hasSpam) {
return NextResponse.json(
{ error: 'Message blocked by spam filter' },
{ status: 400 }
)
}

// In a real application, you would send an email here using a service like:
// - Resend (https://resend.com/)
// - SendGrid
// - Mailgun
// - AWS SES

// Example with a hypothetical email service:
/*
await emailService.send({
to: 'hello@agencypro.com',
subject: `New Contact Form Submission from ${validatedData.name}`,
html: `
<h2>New Contact Form Submission</h2>
<p><strong>Name:</strong> ${validatedData.name}</p>
<p><strong>Email:</strong> ${validatedData.email}</p>
${validatedData.company ? `<p><strong>Company:</strong> ${validatedData.company}</p>` : ''}
${validatedData.phone ? `<p><strong>Phone:</strong> ${validatedData.phone}</p>` : ''}
${validatedData.budget ? `<p><strong>Budget:</strong> ${validatedData.budget}</p>` : ''}
<p><strong>Message:</strong></p>
<p>${validatedData.message.replace(/\n/g, '<br>')}</p>
<hr>
<p><small>IP: ${ip}</small></p>
<p><small>Timestamp: ${new Date().toISOString()}</small></p>
`
})

// Send auto-reply to the user
await emailService.send({
to: validatedData.email,
subject: 'Thank you for contacting us!',
html: `
<h2>Thank you for your message!</h2>
<p>Hi ${validatedData.name},</p>
<p>We've received your message and will get back to you within 24 hours.</p>
<p>Best regards,<br>The AgencyPro Team</p>
`
})
*/

// For now, just log the submission (remove in production)
console.log('Contact form submission:', {
...validatedData,
ip,
timestamp: new Date().toISOString()
})

// You could also save to a database here
// await database.contacts.create(validatedData)

return NextResponse.json({
success: true,
message: 'Message sent successfully'
})

} catch (error) {
console.error('Contact form error:', error)

if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Invalid form data', details: error.errors },
{ status: 400 }
)
}

return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}

// Handle unsupported methods
export async function GET() {
return NextResponse.json(
{ error: 'Method not allowed' },
{ status: 405 }
)
}
10 changes: 7 additions & 3 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from 'next'
import localFont from 'next/font/local'
import './globals.css'
import { Toaster } from '@/components/ui/sonner'

const geistSans = localFont({
src: './fonts/GeistVF.woff',
Expand All @@ -14,8 +15,8 @@ const geistMono = localFont({
})

export const metadata: Metadata = {
title: 'CodeGuide Starter Pro',
description: 'Starter kit from codeguide.dev',
title: 'AgencyPro - Digital Solutions Agency',
description: 'We create exceptional web applications and digital experiences that drive growth.',
}

export default function RootLayout({
Expand All @@ -25,7 +26,10 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>{children}</body>
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
{children}
<Toaster richColors position="top-center" />
</body>
</html>
)
}
17 changes: 15 additions & 2 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
"use client"

import { Header } from '@/components/navigation/header'
import { Hero } from '@/components/ui/animated-hero'
import Image from 'next/image'
import { ServicesSection } from '@/components/sections/services-section'
import { PortfolioSection } from '@/components/sections/portfolio-section'
import { AboutSection } from '@/components/sections/about-section'
import { ContactSection } from '@/components/sections/contact-section'

export default function Home() {
return <Hero />
return (
<div className="min-h-screen">
<Header />
<Hero />
<ServicesSection />
<PortfolioSection />
<AboutSection />
<ContactSection />
</div>
)
}
159 changes: 159 additions & 0 deletions components/navigation/header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
'use client'

import { useState, useEffect } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { Menu, X } from 'lucide-react'
import { Button } from '@/components/ui/button'

const navigation = [
{ name: 'Home', href: '#hero' },
{ name: 'Services', href: '#services' },
{ name: 'Portfolio', href: '#portfolio' },
{ name: 'About', href: '#about' },
{ name: 'Contact', href: '#contact' },
]

export function Header() {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const [scrolled, setScrolled] = useState(false)

useEffect(() => {
const handleScroll = () => {
const isScrolled = window.scrollY > 50
setScrolled(isScrolled)
}

window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [])

const scrollToSection = (href: string) => {
const element = document.querySelector(href)
if (element) {
element.scrollIntoView({ behavior: 'smooth' })
}
setMobileMenuOpen(false)
}

return (
<header
className={`fixed inset-x-0 top-0 z-50 transition-all duration-300 ${
scrolled ? 'bg-background/95 backdrop-blur-md border-b border-border/50' : 'bg-transparent'
}`}
>
<nav className="mx-auto flex max-w-7xl items-center justify-between p-6 lg:px-8" aria-label="Global">
<div className="flex lg:flex-1">
<button
onClick={() => scrollToSection('#hero')}
className="flex items-center gap-2 -m-1.5 p-1.5 transition-opacity hover:opacity-80"
>
<span className="sr-only">Your Company</span>
<div className="h-8 w-8 rounded-lg bg-primary/10 flex items-center justify-center">
<div className="h-4 w-4 rounded bg-gradient-to-br from-blue-600 to-purple-600"></div>
</div>
<span className="text-xl font-bold">AgencyPro</span>
</button>
</div>

<div className="flex lg:hidden">
<button
type="button"
className="-m-2.5 inline-flex items-center justify-center rounded-md p-2.5 text-gray-700"
onClick={() => setMobileMenuOpen(true)}
aria-label="Open main menu"
>
<Menu className="h-6 w-6" aria-hidden="true" />
</button>
</div>

<div className="hidden lg:flex lg:gap-x-12">
{navigation.map((item) => (
<button
key={item.name}
onClick={() => scrollToSection(item.href)}
className="text-sm font-semibold leading-6 text-foreground hover:text-primary transition-colors"
>
{item.name}
</button>
))}
</div>

<div className="hidden lg:flex lg:flex-1 lg:justify-end">
<Button
onClick={() => scrollToSection('#contact')}
className="gap-2"
>
Get Started
</Button>
</div>
</nav>

<AnimatePresence>
{mobileMenuOpen && (
<motion.div
initial={{ opacity: 0, x: '100%' }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: '100%' }}
transition={{ type: 'tween', duration: 0.3 }}
className="lg:hidden fixed inset-y-0 right-0 z-50 w-full overflow-y-auto bg-background px-6 py-6 sm:max-w-sm sm:ring-1 sm:ring-border/50"
>
<div className="flex items-center justify-between">
<button
onClick={() => {
scrollToSection('#hero')
setMobileMenuOpen(false)
}}
className="flex items-center gap-2 -m-1.5 p-1.5"
>
<span className="sr-only">Your Company</span>
<div className="h-8 w-8 rounded-lg bg-primary/10 flex items-center justify-center">
<div className="h-4 w-4 rounded bg-gradient-to-br from-blue-600 to-purple-600"></div>
</div>
<span className="text-xl font-bold">AgencyPro</span>
</button>
<button
type="button"
className="-m-2.5 rounded-md p-2.5 text-gray-700"
onClick={() => setMobileMenuOpen(false)}
aria-label="Close menu"
>
<X className="h-6 w-6" aria-hidden="true" />
</button>
</div>
<div className="mt-6 flow-root">
<div className="-my-6 divide-y divide-border/50">
<div className="space-y-2 py-6">
{navigation.map((item) => (
<button
key={item.name}
onClick={() => scrollToSection(item.href)}
className="block w-full text-left rounded-lg px-3 py-2 text-base font-semibold leading-7 text-foreground hover:bg-muted transition-colors"
>
{item.name}
</button>
))}
</div>
<div className="py-6">
<Button
onClick={() => scrollToSection('#contact')}
className="w-full gap-2"
>
Get Started
</Button>
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>

{mobileMenuOpen && (
<div
className="fixed inset-0 z-40 bg-black/20 backdrop-blur-sm lg:hidden"
onClick={() => setMobileMenuOpen(false)}
aria-hidden="true"
/>
)}
</header>
)
}
Loading