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
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"preview": "vite preview"
},
"dependencies": {
"lucide-react": "^1.12.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
Expand Down
19 changes: 19 additions & 0 deletions src/components/BrandMark.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { MessageCircleMore } from 'lucide-react'

function BrandMark({ compact = false }) {
return (
<div className="inline-flex items-center gap-3">
<span className="inline-flex h-10 w-10 items-center justify-center rounded-lg bg-primary-700 text-white shadow-sm shadow-primary-900/20">
<MessageCircleMore className="h-5 w-5" aria-hidden="true" />
</span>
<span>
<span className="block text-2xl font-bold leading-none tracking-normal text-stone-950">Haggly</span>
{!compact && (
<span className="mt-1 block text-sm font-medium text-stone-500">AI negotiation workspace</span>
)}
</span>
</div>
)
}

export default BrandMark
24 changes: 8 additions & 16 deletions src/components/CopyButton.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { useState } from 'react'
import { Check, Copy } from 'lucide-react'
import Button from './ui/Button'

function CopyButton({ text, onCopy }) {
const [copied, setCopied] = useState(false)
Expand Down Expand Up @@ -35,33 +37,23 @@ function CopyButton({ text, onCopy }) {
}

return (
<button
<Button
onClick={handleCopy}
className={`
flex items-center gap-2 px-4 py-2 rounded-lg font-medium text-sm
transition-all duration-200 active:scale-95
${copied
? 'bg-green-100 text-green-700 border-2 border-green-200'
: 'bg-primary-100 text-primary-700 border-2 border-primary-200 hover:bg-primary-200'
}
`}
variant={copied ? 'primary' : 'muted'}
size="sm"
>
{copied ? (
<>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<Check className="h-4 w-4" aria-hidden="true" />
Copied!
</>
) : (
<>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
</svg>
<Copy className="h-4 w-4" aria-hidden="true" />
Copy
</>
)}
</button>
</Button>
)
}

Expand Down
63 changes: 25 additions & 38 deletions src/components/InputForm.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { useState } from 'react'
import { LoaderCircle, Sparkles } from 'lucide-react'
import Button from './ui/Button'

function InputForm({ onGenerate, isGenerating }) {
const [formData, setFormData] = useState({
Expand Down Expand Up @@ -59,23 +61,22 @@ function InputForm({ onGenerate, isGenerating }) {
}

const inputClasses = (fieldName) => `
w-full px-4 py-3 rounded-xl border-2 transition-all duration-200
w-full rounded-lg border px-4 py-3 transition-all duration-200
${errors[fieldName]
? 'border-red-300 bg-red-50 focus:border-red-500 focus:ring-red-500/20'
: 'border-gray-200 bg-gray-50 focus:border-primary-500 focus:bg-white focus:ring-primary-500/20'
: 'border-stone-200 bg-stone-50 focus:border-primary-500 focus:bg-white focus:ring-primary-500/20'
}
focus:ring-4 focus:outline-none
placeholder:text-gray-400
placeholder:text-stone-400
`

const labelClasses = "block text-sm font-medium text-gray-700 mb-1.5"
const labelClasses = "block text-sm font-semibold text-stone-700 mb-1.5"

return (
<form onSubmit={handleSubmit} className="space-y-5">
{/* Item Name */}
<div>
<label htmlFor="item" className={labelClasses}>
What are you selling? 📦
What are you selling?
</label>
<input
type="text"
Expand All @@ -88,19 +89,17 @@ function InputForm({ onGenerate, isGenerating }) {
autoComplete="off"
/>
{errors.item && (
<p className="text-red-500 text-sm mt-1">{errors.item}</p>
<p className="text-red-600 text-sm mt-1">{errors.item}</p>
)}
</div>

{/* Price Inputs Grid */}
<div className="grid grid-cols-2 gap-4">
{/* Asking Price */}
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label htmlFor="askingPrice" className={labelClasses}>
Your asking price 💰
Your asking price
</label>
<div className="relative">
<span className="absolute left-4 top-1/2 transform -translate-y-1/2 text-gray-400 font-medium">
<span className="absolute left-4 top-1/2 transform -translate-y-1/2 text-stone-400 font-medium">
$
</span>
<input
Expand All @@ -116,17 +115,16 @@ function InputForm({ onGenerate, isGenerating }) {
/>
</div>
{errors.askingPrice && (
<p className="text-red-500 text-sm mt-1">{errors.askingPrice}</p>
<p className="text-red-600 text-sm mt-1">{errors.askingPrice}</p>
)}
</div>

{/* Their Offer */}
<div>
<label htmlFor="theirOffer" className={labelClasses}>
Their offer 🤔
Their offer
</label>
<div className="relative">
<span className="absolute left-4 top-1/2 transform -translate-y-1/2 text-gray-400 font-medium">
<span className="absolute left-4 top-1/2 transform -translate-y-1/2 text-stone-400 font-medium">
$
</span>
<input
Expand All @@ -142,19 +140,18 @@ function InputForm({ onGenerate, isGenerating }) {
/>
</div>
{errors.theirOffer && (
<p className="text-red-500 text-sm mt-1">{errors.theirOffer}</p>
<p className="text-red-600 text-sm mt-1">{errors.theirOffer}</p>
)}
</div>
</div>

{/* Minimum Price (Optional) */}
<div>
<label htmlFor="minimumPrice" className={labelClasses}>
Your minimum acceptable price
<span className="text-gray-400 font-normal ml-1">(optional)</span>
<span className="text-stone-400 font-normal ml-1">(optional)</span>
</label>
<div className="relative">
<span className="absolute left-4 top-1/2 transform -translate-y-1/2 text-gray-400 font-medium">
<span className="absolute left-4 top-1/2 transform -translate-y-1/2 text-stone-400 font-medium">
$
</span>
<input
Expand All @@ -170,41 +167,31 @@ function InputForm({ onGenerate, isGenerating }) {
/>
</div>
{errors.minimumPrice && (
<p className="text-red-500 text-sm mt-1">{errors.minimumPrice}</p>
<p className="text-red-600 text-sm mt-1">{errors.minimumPrice}</p>
)}
<p className="text-gray-400 text-xs mt-1">
<p className="text-stone-400 text-xs mt-1">
The lowest you'd go before walking away
</p>
</div>

{/* Submit Button */}
<button
<Button
type="submit"
disabled={isGenerating}
className={`
w-full py-4 px-6 rounded-xl font-semibold text-white text-lg
transition-all duration-200
${isGenerating
? 'bg-primary-400 cursor-not-allowed'
: 'bg-gradient-to-r from-primary-500 to-primary-600 hover:from-primary-600 hover:to-primary-700 active:scale-[0.98] shadow-lg shadow-primary-500/30 hover:shadow-xl hover:shadow-primary-500/40'
}
`}
size="lg"
className="w-full"
>
{isGenerating ? (
<span className="flex items-center justify-center gap-2">
<svg className="animate-spin h-5 w-5" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
<LoaderCircle className="h-5 w-5 animate-spin" aria-hidden="true" />
Generating...
</span>
) : (
<span className="flex items-center justify-center gap-2">
<span>✨</span>
<Sparkles className="h-5 w-5" aria-hidden="true" />
Generate Responses
</span>
)}
</button>
</Button>
</form>
)
}
Expand Down
40 changes: 20 additions & 20 deletions src/components/ResponseCard.jsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
import CopyButton from './CopyButton'

function ResponseCard({ tone, emoji, message, description, onCopy, delay = 0 }) {
function ResponseCard({ tone, message, description, onCopy, delay = 0 }) {
const toneColors = {
friendly: {
bg: 'bg-emerald-50',
border: 'border-emerald-200',
badge: 'bg-emerald-100 text-emerald-700',
accent: 'text-emerald-600'
bg: 'bg-primary-50',
border: 'border-primary-100',
badge: 'bg-primary-100 text-primary-800',
accent: 'text-primary-700',
dot: 'bg-primary-500',
},
firm: {
bg: 'bg-amber-50',
border: 'border-amber-200',
badge: 'bg-amber-100 text-amber-700',
accent: 'text-amber-600'
border: 'border-amber-100',
badge: 'bg-amber-100 text-amber-800',
accent: 'text-amber-800',
dot: 'bg-amber-500',
},
casual: {
bg: 'bg-sky-50',
border: 'border-sky-200',
badge: 'bg-sky-100 text-sky-700',
accent: 'text-sky-600'
bg: 'bg-stone-50',
border: 'border-stone-200',
badge: 'bg-stone-200 text-stone-800',
accent: 'text-stone-600',
dot: 'bg-stone-500',
}
}

Expand All @@ -27,30 +30,27 @@ function ResponseCard({ tone, emoji, message, description, onCopy, delay = 0 })
return (
<div
className={`
${colors.bg} ${colors.border} border-2 rounded-2xl p-5
${colors.bg} ${colors.border} rounded-lg border p-5
card-hover animate-slide-up
`}
style={{ animationDelay: `${delay}ms` }}
>
{/* Header */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<span className="text-xl">{emoji}</span>
<span className={`px-3 py-1 rounded-full text-sm font-semibold ${colors.badge}`}>
<span className={`h-2.5 w-2.5 rounded-full ${colors.dot}`} />
<span className={`rounded-full px-3 py-1 text-sm font-semibold ${colors.badge}`}>
{tone}
</span>
</div>
<CopyButton text={message} onCopy={onCopy} />
</div>

{/* Message */}
<div className="bg-white/70 rounded-xl p-4 border border-white">
<p className="text-gray-800 leading-relaxed whitespace-pre-wrap">
<div className="border-l-2 border-current/20 pl-4">
<p className="text-stone-800 leading-relaxed whitespace-pre-wrap">
{message}
</p>
</div>

{/* Description */}
{description && (
<p className={`mt-3 text-sm ${colors.accent}`}>
{description}
Expand Down
8 changes: 4 additions & 4 deletions src/components/Toast.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { Check } from 'lucide-react'

function Toast({ message }) {
if (!message) {
return null
}

return (
<div className="fixed top-4 left-1/2 transform -translate-x-1/2 z-50 animate-slide-up">
<div className="bg-green-600 text-white px-6 py-3 rounded-full shadow-lg flex items-center gap-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<div className="bg-primary-800 text-white px-5 py-3 rounded-full shadow-lg shadow-primary-900/20 flex items-center gap-2">
<Check className="h-4 w-4" aria-hidden="true" />
{message}
</div>
</div>
Expand Down
40 changes: 40 additions & 0 deletions src/components/ui/Button.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const variantClasses = {
primary: 'bg-primary-700 text-white hover:bg-primary-800 shadow-sm shadow-primary-900/15',
secondary: 'border border-primary-200 bg-white text-primary-900 hover:border-primary-300 hover:bg-primary-50',
ghost: 'text-primary-800 hover:bg-primary-50',
muted: 'bg-primary-50 text-primary-800 hover:bg-primary-100',
}

const sizeClasses = {
sm: 'h-9 px-3 text-sm',
md: 'h-11 px-4 text-sm',
lg: 'min-h-14 px-5 py-3 text-base',
}

function Button({
children,
className = '',
variant = 'primary',
size = 'md',
type = 'button',
...props
}) {
return (
<button
type={type}
className={[
'inline-flex items-center justify-center gap-2 rounded-lg font-semibold transition',
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',
'disabled:cursor-not-allowed disabled:opacity-65',
variantClasses[variant],
sizeClasses[size],
className,
].join(' ')}
{...props}
>
{children}
</button>
)
}

export default Button
9 changes: 9 additions & 0 deletions src/components/ui/Surface.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
function Surface({ children, className = '' }) {
return (
<div className={`rounded-lg border border-stone-200 bg-white shadow-sm shadow-stone-900/5 ${className}`}>
{children}
</div>
)
}

export default Surface
Loading