Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/pitch and story view #220

Merged
merged 7 commits into from
Feb 27, 2025
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: 62 additions & 0 deletions apps/web/app/(routes)/project/[id]/story/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Save } from 'lucide-react'
import { Button } from '../../../../../components/base/button'
import { ProjectMedia } from '../../../../../components/sections/project/project-media'
import { ProjectStoryForm } from '../../../../../components/sections/project/project-story'
import { ProjectTips } from '../../../../../components/sections/project/project-tips'
import type { ProjectStory } from '../../../../../lib/validators/project'

export default function ProjectPitchPage() {
const handleStorySubmit = (data: ProjectStory) => {
// TODO: Implement story submission
console.log('Story submitted:', data)
}

const handleFileUpload = (file: File) => {
// TODO: Implement file upload
console.log('File uploaded:', file.name)
}

const handleVideoUrlChange = (url: string) => {
// TODO: Implement video URL update
console.log('Video URL updated:', url)
}

const handleSaveAll = () => {
// TODO: Implement saving all changes
console.log('Saving all changes')
}

return (
<div className="container mx-auto py-8">
<h1 className="text-3xl font-bold mb-8">Project Pitch & Story</h1>
<p className="text-gray-600 mb-8">
Tell your story and explain why donors should support your project
</p>

<div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
<div className="lg:col-span-3">
<div className="space-y-8">
<ProjectStoryForm onSubmit={handleStorySubmit} />
<ProjectMedia
onFileUpload={handleFileUpload}
onVideoUrlChange={handleVideoUrlChange}
/>
</div>
<div className="mt-8 flex justify-end">
<Button
onClick={handleSaveAll}
className="bg-black hover:bg-black/90 text-white"
>
<Save className="w-4 h-4 mr-1" />
Save Changes
</Button>
</div>
</div>

<aside className="lg:col-span-1 relative">
<ProjectTips />
</aside>
</div>
</div>
)
}
2 changes: 2 additions & 0 deletions apps/web/components/base/input.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client'

import { composeEventHandlers } from '@radix-ui/primitive'
import { composeRefs } from '@radix-ui/react-compose-refs'
import { Primitive } from '@radix-ui/react-primitive'
Expand Down
83 changes: 83 additions & 0 deletions apps/web/components/sections/project/project-media.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
'use client'

import { useCallback } from 'react'
import { useDropzone } from 'react-dropzone'
import { Card } from '../../base/card'
import { Input } from '../../base/input'

interface ProjectMediaProps {
onFileUpload: (file: File) => void
onVideoUrlChange: (url: string) => void
videoUrl?: string
}

export function ProjectMedia({
onFileUpload,
onVideoUrlChange,
videoUrl = '',
}: ProjectMediaProps) {
const onDrop = useCallback(
(acceptedFiles: File[]) => {
const file = acceptedFiles[0]
if (file && file.size <= 10 * 1024 * 1024) {
// 10MB limit
onFileUpload(file)
}
},
[onFileUpload],
)

const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: {
'application/pdf': ['.pdf'],
'application/vnd.openxmlformats-officedocument.presentationml.presentation':
['.pptx'],
},
maxFiles: 1,
maxSize: 10 * 1024 * 1024, // 10MB
})

return (
<Card className="p-6 shadow-md hover:shadow-lg transition-shadow">
<h3 className="text-xl font-semibold mb-4">Media & Attachments</h3>
<div className="space-y-6">
<div>
<h4 className="font-medium mb-2">Pitch Deck</h4>
<div
{...getRootProps()}
className={`border-2 border-dashed rounded-lg p-6 text-center cursor-pointer transition-colors ${
isDragActive
? 'border-blue-500 bg-blue-50'
: 'border-gray-300 hover:border-gray-400'
}`}
>
<input {...getInputProps()} />
<p className="text-gray-600">
{isDragActive
? 'Drop the file here...'
: 'Drag & drop your pitch deck here, or click to select'}
</p>
<p className="text-sm text-gray-500 mt-1">
PDF or PowerPoint, max 10MB
</p>
</div>
</div>

<div>
<h4 className="font-medium mb-2">Video URL</h4>
<Input
type="url"
placeholder="Enter URL to your pitch video (YouTube, Vimeo, etc.)"
value={videoUrl}
onChange={(e) => onVideoUrlChange(e.target.value)}
className="w-full"
/>
<p className="text-sm text-gray-500 mt-1">
Optional: Add a video to enhance your pitch
</p>
</div>
</div>
</Card>
)
}
73 changes: 73 additions & 0 deletions apps/web/components/sections/project/project-story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
'use client'

import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import { RichTextEditor } from '../../../components/sections/project/rich-text-editor'
import {
type ProjectStory,
projectStorySchema,
} from '../../../lib/validators/project'
import { Card } from '../../base/card'
import { Input } from '../../base/input'

interface ProjectStoryFormProps {
onSubmit: (data: ProjectStory) => void
initialData?: Partial<ProjectStory>
}

export function ProjectStoryForm({
onSubmit,
initialData,
}: ProjectStoryFormProps) {
const {
register,
handleSubmit,
watch,
setValue,
formState: { errors },
} = useForm<ProjectStory>({
resolver: zodResolver(projectStorySchema),
defaultValues: {
title: initialData?.title || '',
story: initialData?.story || '',
},
})

const title = watch('title')

return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
<Card className="p-6 shadow-md hover:shadow-lg transition-shadow">
<h3 className="text-xl font-semibold mb-4">Project Story</h3>
<div className="space-y-4">
<div>
<Input
{...register('title')}
placeholder="Enter compelling title for your project"
className="w-full"
/>
<div className="flex justify-between mt-1">
{errors.title && (
<p className="text-sm text-red-500">{errors.title.message}</p>
)}
<p className="text-sm text-gray-500">
{title?.length || 0}/100 characters
</p>
</div>
</div>
<div>
<RichTextEditor
content={initialData?.story || ''}
onChange={(content) => setValue('story', content)}
/>
{errors.story && (
<p className="text-sm text-red-500 mt-1">
{errors.story.message}
</p>
)}
</div>
</div>
</Card>
</form>
)
}
41 changes: 41 additions & 0 deletions apps/web/components/sections/project/project-tips.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { ChevronRight, Lightbulb, Star } from 'lucide-react'
import { Card } from '../../base/card'

export function ProjectTips() {
return (
<div className="space-y-6 sticky top-24">
<Card className="p-6 shadow-md hover:shadow-lg transition-shadow">
<div className="flex items-center gap-2 mb-4">
<Star className="w-5 h-5 text-yellow-500" />
<h3 className="text-xl font-semibold">Writing Tips</h3>
</div>
<ul className="space-y-3">
{[
'Start with a compelling problem statement',
'Explain your unique solution clearly',
'Include specific impact metric goals',
"Share your team's expertise and passion",
'Use visuals to support narrative',
'End with a clear call to action',
].map((tip) => (
<li key={tip} className="flex items-start gap-2">
<ChevronRight className="w-4 h-4 mt-1 text-blue-500 shrink-0" />
<span>{tip}</span>
</li>
))}
</ul>
</Card>

<Card className="p-6 shadow-md hover:shadow-lg transition-shadow">
<div className="flex items-center gap-2 mb-2">
<Lightbulb className="w-5 h-5 text-yellow-500" />
<h3 className="text-lg font-semibold">Pro Tip</h3>
</div>
<p className="text-gray-600">
Keep your pitch concise and focused. Use data and stories to
demonstrate your impactful potential.
</p>
</Card>
</div>
)
}
Loading
Loading