improved all pages and changed content to match syllabus

This commit is contained in:
Harivansh Rathi 2024-11-29 13:05:39 -05:00
parent 43bd803759
commit b9eb7cf738
32 changed files with 1290 additions and 300 deletions

View file

@ -5,12 +5,12 @@ import Home from './pages/Home';
import Blogs from './pages/Blogs';
import BlogPost from './pages/BlogPost/BlogPost';
import { ErrorBoundary } from './components/ErrorBoundary';
import SuccessStories from './pages/SuccessStories';
// Lazy load other pages
const Quiz = React.lazy(() => import('./pages/Quiz'));
const Advice = React.lazy(() => import('./pages/Advice'));
const Vendors = React.lazy(() => import('./pages/Vendors'));
const Stories = React.lazy(() => import('./pages/Stories'));
const MarketCalculator = React.lazy(() => import('./pages/MarketCalculator'));
function App() {
@ -26,7 +26,7 @@ function App() {
<Route path="/quiz" element={<Quiz />} />
<Route path="/advice" element={<Advice />} />
<Route path="/vendors" element={<Vendors />} />
<Route path="/stories" element={<Stories />} />
<Route path="/success-stories" element={<SuccessStories />} />
<Route path="/calculator" element={<MarketCalculator />} />
</Routes>
</Suspense>

View file

@ -1,4 +1,5 @@
import { Link } from 'react-router-dom';
import { Toaster } from '@/components/ui/toaster';
const MainLayout = ({ children }: { children: React.ReactNode }) => {
return (
@ -16,7 +17,7 @@ const MainLayout = ({ children }: { children: React.ReactNode }) => {
<Link to="/quiz" className="nav-link">Bride Quiz</Link>
<Link to="/advice" className="nav-link">Dear Jane</Link>
<Link to="/vendors" className="nav-link">Vendors</Link>
<Link to="/stories" className="nav-link">Success Stories</Link>
<Link to="/success-stories" className="nav-link">Success Stories</Link>
<Link to="/calculator" className="nav-link">Market Value</Link>
</div>
</div>
@ -35,6 +36,8 @@ const MainLayout = ({ children }: { children: React.ReactNode }) => {
</div>
</div>
</footer>
<Toaster />
</div>
);
};

View file

@ -1,8 +1,6 @@
import * as React from 'react';
import { Cross2Icon } from '@radix-ui/react-icons';
import * as ToastPrimitives from '@radix-ui/react-toast';
import { cva, type VariantProps } from 'class-variance-authority';
import { Cross2Icon } from '@radix-ui/react-icons';
import { cn } from '@/lib/utils';
const ToastProvider = ToastPrimitives.Provider;
@ -22,52 +20,27 @@ const ToastViewport = React.forwardRef<
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva(
'group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
{
variants: {
variant: {
default: 'border bg-background text-foreground',
destructive:
'destructive group border-destructive bg-destructive text-destructive-foreground',
},
},
defaultVariants: {
variant: 'default',
},
}
);
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & {
variant?: 'default' | 'destructive';
}
>(({ className, variant = 'default', ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
className={cn(
'group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all',
variant === 'default' && 'border-sage-200 bg-white text-sage-900',
variant === 'destructive' && 'border-red-500 bg-red-50 text-red-900',
className
)}
{...props}
/>
);
});
Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive',
className
)}
{...props}
/>
));
ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
@ -75,7 +48,7 @@ const ToastClose = React.forwardRef<
<ToastPrimitives.Close
ref={ref}
className={cn(
'absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',
'absolute right-1 top-1 rounded-md p-1 text-sage-500 opacity-0 transition-opacity hover:text-sage-900 focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100',
className
)}
toast-close=""
@ -92,7 +65,7 @@ const ToastTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn('text-sm font-semibold [&+div]:text-xs', className)}
className={cn('text-sm font-semibold', className)}
{...props}
/>
));
@ -110,18 +83,11 @@ const ToastDescription = React.forwardRef<
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement = React.ReactElement<typeof ToastAction>;
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
};

View file

@ -1,4 +1,3 @@
import { useToast } from '@/hooks/use-toast';
import {
Toast,
ToastClose,
@ -7,13 +6,14 @@ import {
ToastTitle,
ToastViewport,
} from '@/components/ui/toast';
import { useToast } from '@/components/ui/use-toast';
export function Toaster() {
const { toasts } = useToast();
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
{toasts.map(({ id, title, description, action, ...props }) => {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">

View file

@ -0,0 +1,56 @@
import { useState, useEffect } from 'react';
interface Toast {
id: string;
title?: string;
description?: string;
action?: React.ReactNode;
variant?: 'default' | 'destructive';
}
interface ToastOptions {
title?: string;
description?: string;
action?: React.ReactNode;
variant?: 'default' | 'destructive';
duration?: number;
}
const DEFAULT_TOAST_DURATION = 5000; // 5 seconds
export function useToast() {
const [toasts, setToasts] = useState<Toast[]>([]);
useEffect(() => {
const timeouts = toasts.map((toast) => {
return setTimeout(() => {
setToasts((current) => current.filter((t) => t.id !== toast.id));
}, DEFAULT_TOAST_DURATION);
});
return () => {
timeouts.forEach((timeout) => clearTimeout(timeout));
};
}, [toasts]);
function toast(options: ToastOptions) {
const id = Math.random().toString(36).slice(2);
setToasts((current) => [
...current,
{
id,
...options,
},
]);
}
function dismiss(toastId: string) {
setToasts((current) => current.filter((t) => t.id !== toastId));
}
return {
toasts,
toast,
dismiss,
};
}

83
src/components/vendor/VendorCard.tsx vendored Normal file
View file

@ -0,0 +1,83 @@
import { Star } from 'lucide-react';
import { VendorListing } from '@/types/vendors';
import { cn } from '@/lib/utils';
type VendorCardProps = {
vendor: VendorListing;
onClick?: () => void;
};
const VendorCard = ({ vendor, onClick }: VendorCardProps) => {
const { name, description, category, location, priceRange, rating, imageUrl } = vendor;
const renderRatingStars = (value: number) => {
return Array.from({ length: 5 }).map((_, index) => (
<Star
key={index}
className={cn(
'h-4 w-4',
index < value ? 'fill-sage-500 text-sage-500' : 'text-sage-200'
)}
/>
));
};
return (
<article
onClick={onClick}
className="group bg-cream-50 rounded-lg overflow-hidden shadow-lg transition-all hover:shadow-xl cursor-pointer"
>
<div className="aspect-w-16 aspect-h-9 bg-sage-200">
{imageUrl ? (
<img
src={imageUrl}
alt={name}
className="w-full h-48 object-cover group-hover:opacity-90 transition-opacity"
/>
) : (
<div className="w-full h-48 bg-sage-300 group-hover:opacity-90 transition-opacity">
<div className="w-full h-full flex items-center justify-center text-sage-600 font-cormorant text-xl">
{name}
</div>
</div>
)}
</div>
<div className="p-6 space-y-4">
<div>
<div className="flex justify-between items-start mb-2">
<h2 className="font-cormorant text-2xl text-sage-900">{name}</h2>
<span className="text-sage-700 font-medium">{priceRange}</span>
</div>
<p className="text-sage-700 text-sm mb-2">{location}</p>
<span className="inline-block px-3 py-1 rounded-full text-xs capitalize bg-sage-100 text-sage-700">
{category}
</span>
</div>
<p className="text-sage-700 line-clamp-2">{description}</p>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm text-sage-600">Reputation</span>
<div className="flex">{renderRatingStars(rating.reputation)}</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-sage-600">Elegance</span>
<div className="flex">{renderRatingStars(rating.elegance)}</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-sage-600">Value</span>
<div className="flex">{renderRatingStars(rating.value)}</div>
</div>
</div>
<button className="w-full bg-sage-500 text-white px-4 py-2 rounded-lg hover:bg-sage-600 transition">
View Details
</button>
</div>
</article>
);
};
export default VendorCard;

145
src/components/vendor/VendorModal.tsx vendored Normal file
View file

@ -0,0 +1,145 @@
import { Star, X } from 'lucide-react';
import { VendorListing } from '@/types/vendors';
import { cn } from '@/lib/utils';
type VendorModalProps = {
vendor: VendorListing;
onClose: () => void;
};
const VendorModal = ({ vendor, onClose }: VendorModalProps) => {
const {
name,
description,
category,
location,
priceRange,
rating,
features,
contactPerson,
testimonials,
imageUrl
} = vendor;
const renderRatingStars = (value: number) => {
return Array.from({ length: 5 }).map((_, index) => (
<Star
key={index}
className={cn(
'h-4 w-4',
index < value ? 'fill-sage-500 text-sage-500' : 'text-sage-200'
)}
/>
));
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
<div className="relative w-full max-w-4xl max-h-[90vh] overflow-y-auto bg-cream-50 rounded-lg shadow-xl">
<button
onClick={onClose}
className="absolute right-4 top-4 p-2 rounded-full hover:bg-sage-100 transition-colors"
>
<X className="h-6 w-6 text-sage-700" />
</button>
<div className="p-8">
<div className="mb-8">
{imageUrl ? (
<img
src={imageUrl}
alt={name}
className="w-full h-64 object-cover rounded-lg"
/>
) : (
<div className="w-full h-64 bg-sage-300 rounded-lg flex items-center justify-center">
<span className="text-sage-600 font-cormorant text-2xl">{name}</span>
</div>
)}
</div>
<div className="mb-8">
<div className="flex justify-between items-start mb-4">
<div>
<h2 className="font-cormorant text-3xl text-sage-900 mb-2">{name}</h2>
<p className="text-sage-700">{location}</p>
</div>
<span className="text-sage-700 font-medium text-lg">{priceRange}</span>
</div>
<span className="inline-block px-3 py-1 rounded-full text-sm capitalize bg-sage-100 text-sage-700">
{category}
</span>
</div>
<div className="grid md:grid-cols-2 gap-8">
<div className="space-y-6">
<div>
<h3 className="font-cormorant text-xl text-sage-900 mb-2">About</h3>
<p className="text-sage-700">{description}</p>
</div>
<div>
<h3 className="font-cormorant text-xl text-sage-900 mb-2">Features</h3>
<ul className="list-disc list-inside space-y-1 text-sage-700">
{features.map((feature, index) => (
<li key={index}>{feature}</li>
))}
</ul>
</div>
{contactPerson && (
<div>
<h3 className="font-cormorant text-xl text-sage-900 mb-2">Contact Person</h3>
<p className="text-sage-700">{contactPerson}</p>
</div>
)}
</div>
<div className="space-y-6">
<div>
<h3 className="font-cormorant text-xl text-sage-900 mb-4">Ratings</h3>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sage-700">Reputation</span>
<div className="flex">{renderRatingStars(rating.reputation)}</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sage-700">Elegance</span>
<div className="flex">{renderRatingStars(rating.elegance)}</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sage-700">Value</span>
<div className="flex">{renderRatingStars(rating.value)}</div>
</div>
</div>
</div>
<div>
<h3 className="font-cormorant text-xl text-sage-900 mb-4">Testimonials</h3>
<div className="space-y-4">
{testimonials.map((testimonial, index) => (
<blockquote key={index} className="border-l-2 border-sage-200 pl-4">
<p className="text-sage-700 italic mb-2">{testimonial.text}</p>
<footer className="text-sage-600 text-sm"> {testimonial.author}</footer>
</blockquote>
))}
</div>
</div>
</div>
</div>
<div className="mt-8 flex justify-end">
<button
className="bg-sage-500 text-white px-6 py-2 rounded-lg hover:bg-sage-600 transition"
onClick={onClose}
>
Close
</button>
</div>
</div>
</div>
</div>
);
};
export default VendorModal;

View file

@ -4,51 +4,55 @@ export const blogPosts = {
id: '1',
title: 'The Pragmatic Path to Marriage',
author: 'Charlotte Lucas',
authorImage: 'https://images.unsplash.com/photo-1551434678-e076c223a692?w=800&dpr=2&q=80',
authorImage: '/images/authors/charlotte.jpg',
date: '1813-01-15',
content: [
'My dear readers, I must confess that happiness in marriage is entirely a matter of chance. There will always be vexation and grief, and it is better to know as little as possible of the defects of the person with whom you are to pass your life.',
'I am not romantic, you know. I never was. I ask only a comfortable home; and considering Mr. Collins\'s character, connections, and situation in life, I am convinced that my chance of happiness with him is as fair as most people can boast on entering the marriage state.',
'Let us be practical in our pursuit of matrimony. A woman must secure her future while she has the power to do so. Youth and beauty fade, but a stable position in society endures.'
]
},
{
id: '2',
title: 'On the Management of Expectations',
author: 'Charlotte Lucas',
authorImage: 'https://images.unsplash.com/photo-1551434678-e076c223a692?w=800&dpr=2&q=80',
date: '1813-02-01',
content: [
'Marriage, my dear readers, is a business arrangement first and foremost. Let us not delude ourselves with notions of passionate love that novels so often promote.',
'I have observed that those who enter marriage with the highest expectations often find themselves the most disappointed. Better to approach it with clear eyes and practical goals.',
'Consider your position, your prospects, and your future security. These are the foundations upon which a lasting marriage is built.'
'Let us be practical in our pursuit of matrimony. A woman must secure her future while she has the power to do so.'
]
}
],
marianne: [
{
id: '3',
id: '2',
title: 'The Heart Must Lead',
author: 'Marianne Dashwood',
authorImage: 'https://images.unsplash.com/photo-1551431009-a802eeec77b1?w=800&dpr=2&q=80',
authorImage: '/images/authors/marianne.jpg',
date: '1813-03-15',
content: [
'To love is to burn, to be on fire! How can one possibly consider marriage without the deepest feelings of love and devotion?',
'I could not be happy with a man whose taste did not in every point coincide with my own. He must enter into all my feelings; the same books, the same music must charm us both.',
'Let others speak of prudence and practicality, but I shall never sacrifice the dictates of my heart to the opinions of the world.'
]
},
}
],
fanny: [
{
id: '3',
title: 'On Principle and Affection',
author: 'Fanny Price',
authorImage: '/images/authors/fanny.jpg',
date: '1814-01-15',
content: [
'We must all be guided by our own understanding of what is right. No circumstance should persuade us to act against our principles.',
'True affection grows slowly, nurtured by shared values and mutual respect. It cannot be forced or rushed by the expectations of others.',
'In matters of the heart, we must trust our own judgment above all else.'
]
}
],
catherine: [
{
id: '4',
title: 'Reflections on Love and Second Attachments',
author: 'Marianne Dashwood',
authorImage: 'https://images.unsplash.com/photo-1551431009-a802eeec77b1?w=800&dpr=2&q=80',
date: '1813-04-01',
title: 'Romance and Reality',
author: 'Catherine Morland',
authorImage: '/images/authors/catherine.jpg',
date: '1814-02-15',
content: [
'I once believed that second attachments were impossible, that the heart could love truly only once. How wrong I was!',
'Love, I have learned, can grow quietly, steadily, like a garden tended with care and patience. It need not always burst forth like a sudden flame.',
'To my dear readers, I say: keep your hearts open to the possibility of finding love in unexpected places and unexpected ways.'
'How thrilling it is to discover that real life can be just as fascinating as our favorite novels!',
'Yet we must learn to distinguish between romantic fancy and true character. Not every ancient abbey holds a terrible secret, nor is every charming acquaintance hiding dark mysteries.',
'The greatest adventures may be found in opening our hearts to genuine friendship and love.'
]
}
]
};
};

View file

@ -1,26 +1,156 @@
export const dearJaneLetters = [
export interface DearJaneLetter {
id: string;
from: string;
subject: string;
category: 'courtship' | 'marriage' | 'family' | 'society' | 'heartbreak';
question: string;
answer: string[];
date: string;
relatedBook?: {
title: string;
character: string;
quote: string;
};
}
export const dearJaneLetters: DearJaneLetter[] = [
{
id: '1',
from: 'Hopelessly Romantic',
subject: 'Should I Wait for True Love?',
category: 'courtship',
date: '1813-01-15',
question: 'Dear Jane, I am twenty-seven and have received a proposal from a respectable gentleman. He is kind and well-situated, but I feel no passionate attachment. Should I accept for security\'s sake, or wait for true love?',
answer: [
'My dear Hopelessly Romantic,',
'While the heart must not be entirely silent in matters of marriage, neither should it be the only voice in the conversation. Consider that happiness in marriage is not merely a matter of passionate beginnings, but of compatible temperaments and mutual respect.',
'However, do not mistake mere security for contentment. A marriage without any affection is as imprudent as one based solely on passionate feelings. The ideal lies somewhere between Charlotte Lucas\'s pragmatism and Marianne Dashwood\'s romanticism.',
'Examine your feelings carefully. Is your lack of passion truly indifference, or merely the absence of drama that so many mistake for love? Sometimes the steadiest attachments grow from the most modest beginnings.'
]
],
relatedBook: {
title: 'Sense and Sensibility',
character: 'Marianne Dashwood',
quote: 'The more I know of the world, the more I am convinced that I shall never see a man whom I can really love.'
}
},
{
id: '2',
from: 'Concerned Sister',
subject: 'My Sister\'s Imprudent Attachment',
category: 'family',
date: '1813-02-01',
question: 'Dear Jane, My younger sister has formed an attachment to a gentleman of questionable character. How can I guide her toward prudence without seeming to interfere?',
answer: [
'My dear Concerned Sister,',
'Ah, the delicate art of sisterly guidance! One must tread carefully when matters of the heart are concerned, particularly when dealing with a younger sister who may mistake experience for interference.',
'Remember how our dear Elizabeth Bennet handled her sister Lydia\'s situation. Direct opposition often strengthens such attachments. Instead, guide your sister to examine the gentleman\'s character through his actions rather than his words.',
'Perhaps arrange situations where his true nature might reveal itself naturally. The best advice is often that which allows the recipient to believe they have arrived at the conclusion independently.'
]
],
relatedBook: {
title: 'Pride and Prejudice',
character: 'Elizabeth Bennet',
quote: 'We all know him to be a proud, unpleasant sort of man; but this would be nothing if you really liked him.'
}
},
{
id: '3',
from: 'Socially Anxious',
subject: 'Navigating Social Gatherings',
category: 'society',
date: '1813-03-15',
question: 'Dear Jane, I find myself overwhelmed at social gatherings, particularly when expected to dance and converse with potential suitors. How can I overcome my natural reserve without compromising my dignity?',
answer: [
'My dear Socially Anxious,',
'Take heart in knowing that even the most accomplished among us have felt the weight of social expectations. Consider our dear Anne Elliot, whose quiet dignity and genuine nature eventually won the day over more boisterous displays.',
'Rather than attempting to transform yourself into a social butterfly, focus on meaningful connections. A well-timed observation or thoughtful question often carries more weight than hours of idle chatter.',
'Remember, those worth knowing will appreciate your authentic self. As for dancing, consider it an opportunity for observation rather than a test of social prowess.'
],
relatedBook: {
title: 'Persuasion',
character: 'Anne Elliot',
quote: 'Time will explain.'
}
},
{
id: '4',
from: 'Disappointed in Bath',
subject: 'Recovering from Heartbreak',
category: 'heartbreak',
date: '1813-04-01',
question: 'Dear Jane, I recently discovered that the gentleman I had set my hopes upon is engaged to another. How does one recover from such a disappointment while maintaining one\'s composure in society?',
answer: [
'My dear Disappointed,',
'First, allow me to commend your strength in seeking guidance rather than retreating into despair. The pain of disappointed hopes is keen, but it need not define your future happiness.',
'Consider how our dear Jane Bennet conducted herself when faced with similar circumstances. Her gentle nature and genuine goodwill, even toward those who had caused her pain, preserved both her dignity and her peace of mind.',
'Channel your energies into self-improvement and the cultivation of true friendships. Time, that great healer, works most effectively when we give ourselves permission to grow from our disappointments.'
],
relatedBook: {
title: 'Pride and Prejudice',
character: 'Jane Bennet',
quote: 'I would not wish to be hasty in censuring anyone; but I always speak what I think.'
}
},
{
id: '5',
from: 'Newly Married',
subject: 'Adjusting to Married Life',
category: 'marriage',
date: '1813-05-15',
question: 'Dear Jane, Having recently entered the married state, I find myself struggling to balance my own pursuits with my husband\'s expectations. How can I maintain my independence while building a harmonious partnership?',
answer: [
'My dear Newly Married,',
'The transition from maiden to wife need not mean the abandonment of one\'s individual character. Indeed, the finest marriages are those where both parties encourage each other\'s growth and interests.',
'Consider how Emma Woodhouse and Mr. Knightley maintained their lively debates and individual pursuits even after marriage. Their relationship was strengthened by their ability to challenge and support one another.',
'Establish habits of open communication early in your marriage. A wise partner will value your happiness and independence as much as your devotion.'
],
relatedBook: {
title: 'Emma',
character: 'Emma Woodhouse',
quote: 'I may have lost my heart, but not my self-control.'
}
}
];
];
export interface UserSubmittedQuestion {
id: string;
from: string;
subject: string;
category: DearJaneLetter['category'];
question: string;
date: string;
status: 'pending' | 'answered';
}
export const generateQuestionId = () => {
return `q-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
};
export const saveUserQuestion = (question: Omit<UserSubmittedQuestion, 'id' | 'date' | 'status'>): UserSubmittedQuestion => {
const newQuestion: UserSubmittedQuestion = {
...question,
id: generateQuestionId(),
date: new Date().toISOString(),
status: 'pending'
};
// Get existing questions from localStorage
const existingQuestions = getUserQuestions();
// Add new question to the list
const updatedQuestions = [newQuestion, ...existingQuestions];
// Save back to localStorage
localStorage.setItem('userQuestions', JSON.stringify(updatedQuestions));
return newQuestion;
};
export const getUserQuestions = (): UserSubmittedQuestion[] => {
const stored = localStorage.getItem('userQuestions');
if (!stored) return [];
try {
return JSON.parse(stored);
} catch {
return [];
}
};

View file

@ -10,30 +10,12 @@ export const austenQuotes = [
character: "Charlotte Lucas",
context: "Discussing her practical view of marriage with Elizabeth"
},
{
quote: "If I loved you less, I might be able to talk about it more.",
source: "Emma",
character: "Mr. Knightley",
context: "Confessing his love to Emma"
},
{
quote: "A lady's imagination is very rapid; it jumps from admiration to love, from love to matrimony in a moment.",
source: "Pride and Prejudice",
character: "Mr. Darcy",
context: "Observing how quickly ladies form attachments"
},
{
quote: "The more I know of the world, the more I am convinced that I shall never see a man whom I can really love. I require so much!",
source: "Sense and Sensibility",
character: "Marianne Dashwood",
context: "Expressing her romantic ideals before meeting Colonel Brandon"
},
{
quote: "You pierce my soul. I am half agony, half hope.",
source: "Persuasion",
character: "Captain Wentworth",
context: "His letter to Anne expressing his enduring love"
},
{
quote: "There is nothing I would not do for those who are really my friends. I have no notion of loving people by halves, it is not my nature.",
source: "Northanger Abbey",
@ -41,9 +23,9 @@ export const austenQuotes = [
context: "Demonstrating the difference between words and actions in matters of the heart"
},
{
quote: "A woman is not to marry a man merely because she is asked, or because he is attached to her, and can write a tolerable letter.",
source: "Emma",
character: "Emma Woodhouse",
context: "Advising Harriet Smith about marriage proposals"
quote: "I speak what I think, and therefore may appear abrupt and unpolished.",
source: "Mansfield Park",
character: "Edmund Bertram",
context: "Expressing his honest nature to Fanny Price"
}
];

View file

@ -1,34 +1,89 @@
export const successStories = [
export type SuccessStory = {
id: string;
couple: string;
imageUrl: string;
story: string;
date: string;
location: string;
quote: string;
details: {
ceremony: string;
reception: string;
specialMoments: string[];
};
};
export const successStories: SuccessStory[] = [
{
id: '1',
couple: 'Elizabeth & Darcy',
novel: 'Pride and Prejudice',
image: 'https://images.unsplash.com/photo-1469571486292-0ba58a3f068b?w=800&dpr=2&q=80',
quote: "In vain have I struggled. It will not do. My feelings will not be repressed. You must allow me to tell you how ardently I admire and love you.",
story: "Their journey from mutual prejudice to profound understanding exemplifies how true love can overcome pride and first impressions. Through challenges and misunderstandings, they discovered that real connection grows from honest self-reflection and the courage to change."
id: 'elizabeth-darcy',
couple: 'Elizabeth Bennet & Fitzwilliam Darcy',
imageUrl: '/images/success-stories/elizabeth-darcy.jpg',
story: 'From initial prejudice to profound understanding, Elizabeth and Darcy\'s journey is a testament to how love can overcome pride and preconceptions. Their story began with a series of misunderstandings at the Meryton assembly but blossomed into a deep connection built on mutual respect and admiration.',
date: 'Spring 1813',
location: 'Longbourn Church, Hertfordshire',
quote: "It has been coming on so gradually, that I hardly know when it began.",
details: {
ceremony: 'Traditional ceremony at Longbourn Church',
reception: 'An elegant affair at Pemberley',
specialMoments: [
'First dance at the Netherfield Ball',
'Surprise visit to Pemberley',
'Morning walk in the gardens where they reconciled'
]
}
},
{
id: '2',
couple: 'Emma & Mr. Knightley',
novel: 'Emma',
image: 'https://images.unsplash.com/photo-1515934751635-c81c6bc9a2d8?w=800&dpr=2&q=80',
quote: "If I loved you less, I might be able to talk about it more.",
story: "A tale of friendship blossoming into love, their story shows how the best partnerships are built on mutual respect and the ability to help each other grow. Through Emma's journey of self-discovery, she learns the value of Mr. Knightley's honest guidance."
},
{
id: '3',
couple: 'Anne & Captain Wentworth',
novel: 'Persuasion',
image: 'https://images.unsplash.com/photo-1591604466107-ec97de577aff?w=800&dpr=2&q=80',
quote: "You pierce my soul. I am half agony, half hope.",
story: "Their reunion after eight years proves that true love can withstand time and circumstance. Despite past regrets and the interference of family, their constancy and growth as individuals led them back to each other."
},
{
id: '4',
couple: 'Marianne & Colonel Brandon',
novel: 'Sense and Sensibility',
image: 'https://images.unsplash.com/photo-1595732301236-4a39e6a35b4a?w=800&dpr=2&q=80',
id: 'marianne-brandon',
couple: 'Marianne Dashwood & Colonel Brandon',
imageUrl: '/images/success-stories/marianne-brandon.jpg',
story: 'From youthful romantic ideals to discovering deep and lasting love, Marianne and Colonel Brandon\'s story demonstrates how true love can defy initial impressions and grow from genuine care and devotion.',
date: 'Summer 1814',
location: 'Barton Park Chapel, Devonshire',
quote: "The more I know of the world, the more I am convinced that I shall never see a man whom I can really love.",
story: "Their story demonstrates how love can grow from initial indifference to deep appreciation. Marianne's journey from romantic idealism to finding happiness with the steadfast Colonel Brandon shows how real love often differs from youthful fantasies."
details: {
ceremony: 'Musical ceremony at Barton Park Chapel',
reception: 'Celebration at Delaford',
specialMoments: [
'First pianoforte duet',
'Recovery at Cleveland',
'Autumn walks at Delaford'
]
}
},
{
id: 'edmund-fanny',
couple: 'Edmund Bertram & Fanny Price',
imageUrl: '/images/success-stories/edmund-fanny.jpg',
story: 'A love that grew from childhood friendship to deep understanding, Fanny and Edmund\'s story shows how true worth and genuine feelings triumph over superficial attractions.',
date: 'Autumn 1814',
location: 'Mansfield Park Chapel',
quote: "We have all a better guide in ourselves, if we would attend to it, than any other person can be.",
details: {
ceremony: 'Intimate ceremony at Mansfield Park Chapel',
reception: 'Family gathering at the Parsonage',
specialMoments: [
'Reading together in the East room',
'Walks in the wilderness',
'Return to Mansfield Park'
]
}
},
{
id: 'catherine-henry',
couple: 'Catherine Morland & Henry Tilney',
imageUrl: '/images/success-stories/catherine-henry.jpg',
story: 'What began with a dance at the Bath Assembly Rooms grew into a love story that helped Catherine mature from an imaginative girl into a discerning young woman.',
date: 'Spring 1814',
location: 'Fullerton Parish Church',
quote: "The person, be it gentleman or lady, who has not pleasure in a good novel, must be intolerably stupid.",
details: {
ceremony: 'Charming ceremony at Fullerton',
reception: 'Celebration at Woodston Parsonage',
specialMoments: [
'First dance in Bath',
'Walks around Beechen Cliff',
'Visit to Woodston'
]
}
}
];
];

112
src/data/vendors.ts Normal file
View file

@ -0,0 +1,112 @@
import { VendorListing } from '../types/vendors';
export const VENDOR_LISTINGS: VendorListing[] = [
{
id: '1',
name: 'Pemberley Estate',
description: 'The grandest estate in Derbyshire, offering elegant spaces for the most sophisticated celebrations. Our grounds provide stunning backdrops for ceremonies and receptions.',
category: 'venue',
location: 'Derbyshire',
imageUrl: '/images/vendors/pemberley.jpg',
priceRange: '££££',
rating: {
reputation: 5,
elegance: 5,
value: 4
},
features: [
'Grand ballroom',
'Extensive gardens',
'Lake view',
'Multiple reception rooms',
'Accommodation available'
],
testimonials: [
{
author: 'Elizabeth Bennet',
text: 'The perfect setting for our celebration. The grounds are beyond compare.'
}
]
},
{
id: '2',
name: 'Meryton Assembly Rooms',
description: 'The perfect venue for a country wedding, our assembly rooms have hosted countless memorable celebrations. Known for our excellent acoustics and spacious dance floor.',
category: 'venue',
location: 'Hertfordshire',
imageUrl: '/images/vendors/meryton.jpg',
priceRange: '£££',
rating: {
reputation: 4,
elegance: 4,
value: 5
},
features: [
'Large dance floor',
'Excellent acoustics',
'Catering kitchen',
'Card room',
'Garden access'
],
testimonials: [
{
author: 'Jane Bennet',
text: 'We could not have chosen a better venue. The rooms were perfectly sized for our gathering.'
}
]
},
{
id: '3',
name: 'Barton Park',
description: 'A charming country estate perfect for intimate celebrations. Our grounds offer beautiful views of the Devonshire countryside.',
category: 'venue',
location: 'Devonshire',
imageUrl: '/images/vendors/barton.jpg',
priceRange: '£££',
rating: {
reputation: 4,
elegance: 4,
value: 5
},
features: [
'Country house setting',
'Music room',
'Garden terrace',
'Private chapel',
'Countryside views'
],
testimonials: [
{
author: 'Marianne Dashwood',
text: 'The perfect blend of elegance and natural beauty. The music room is particularly delightful.'
}
]
},
{
id: '4',
name: 'Mansfield Park',
description: 'An elegant estate offering both grandeur and intimacy for your special day. Our chapel and grounds provide multiple options for ceremonies.',
category: 'venue',
location: 'Northamptonshire',
imageUrl: '/images/vendors/mansfield.jpg',
priceRange: '££££',
rating: {
reputation: 5,
elegance: 5,
value: 4
},
features: [
'Private chapel',
'Formal gardens',
'East room',
'Multiple reception spaces',
'Wilderness walk'
],
testimonials: [
{
author: 'Fanny Price',
text: 'The chapel holds such special memories, and the grounds are perfect for quiet moments of reflection.'
}
]
}
];

View file

@ -1,4 +1,4 @@
import { clsx, type ClassValue } from 'clsx';
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {

View file

@ -1,39 +1,231 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Mail } from 'lucide-react';
import { dearJaneLetters } from '@/data/dear-jane';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Mail, Heart, Book, Users, Home, MessageCircle, PenTool, Clock } from 'lucide-react';
import { dearJaneLetters, DearJaneLetter, UserSubmittedQuestion, saveUserQuestion, getUserQuestions } from '@/data/dear-jane';
import { useToast } from '@/components/ui/use-toast';
const categoryIcons = {
courtship: <Heart className="h-4 w-4" />,
marriage: <Users className="h-4 w-4" />,
family: <Home className="h-4 w-4" />,
society: <MessageCircle className="h-4 w-4" />,
heartbreak: <Book className="h-4 w-4" />
};
const categoryNames = {
courtship: 'Matters of Courtship',
marriage: 'Marriage & Partnership',
family: 'Family Relations',
society: 'Social Etiquette',
heartbreak: 'Healing Hearts'
};
const categoryDescriptions = {
courtship: 'Navigate the delicate dance of courtship with Jane\'s guidance on matters of the heart.',
marriage: 'Discover wisdom for maintaining harmony and growth in matrimonial life.',
family: 'Learn to handle family matters with grace, wisdom, and understanding.',
society: 'Master the art of social etiquette while staying true to yourself.',
heartbreak: 'Find comfort and strength in Jane\'s advice for healing a wounded heart.'
};
const DearJane = () => {
const [selectedLetter, setSelectedLetter] = useState(dearJaneLetters[0]);
const { toast } = useToast();
const [selectedLetter, setSelectedLetter] = useState<DearJaneLetter>(dearJaneLetters[0]);
const [selectedCategory, setSelectedCategory] = useState<DearJaneLetter['category']>('courtship');
const [showSubmitForm, setShowSubmitForm] = useState(false);
const [userQuestions, setUserQuestions] = useState<UserSubmittedQuestion[]>([]);
const [formData, setFormData] = useState({
subject: '',
from: '',
question: '',
category: 'courtship' as DearJaneLetter['category']
});
useEffect(() => {
setUserQuestions(getUserQuestions());
}, []);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!formData.subject || !formData.from || !formData.question) {
toast({
title: "Missing Information",
description: "Please fill in all fields before submitting.",
variant: "destructive"
});
return;
}
try {
const newQuestion = saveUserQuestion(formData);
setUserQuestions(prev => [newQuestion, ...prev]);
setShowSubmitForm(false);
setFormData({
subject: '',
from: '',
question: '',
category: 'courtship'
});
toast({
title: "Question Submitted",
description: "Your question has been submitted successfully. Jane will respond soon!"
});
} catch (err) {
console.error('Error submitting question:', err);
toast({
title: "Submission Error",
description: err instanceof Error ? err.message : "There was an error submitting your question. Please try again.",
variant: "destructive"
});
}
};
const allLetters = [...dearJaneLetters, ...userQuestions];
const filteredLetters = allLetters.filter(letter => letter.category === selectedCategory);
return (
<div className="space-y-8">
<div className="space-y-8 max-w-6xl mx-auto">
<header className="text-center space-y-4">
<h1 className="font-serif text-4xl">Dear Jane</h1>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
Seeking matrimonial advice? Let Jane guide you with her timeless wisdom
<h1 className="font-cormorant text-4xl text-sage-900">Dear Jane</h1>
<p className="text-sage-700 max-w-2xl mx-auto">
Seeking counsel in matters of the heart? Let Jane Austen's timeless wisdom guide you through your romantic predicaments.
</p>
<Button
onClick={() => setShowSubmitForm(!showSubmitForm)}
className="bg-sage-500 hover:bg-sage-600 text-white"
>
<PenTool className="h-4 w-4 mr-2" />
{showSubmitForm ? 'Close Form' : 'Ask Jane for Advice'}
</Button>
</header>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 max-w-6xl mx-auto">
<Card className="lg:col-span-1">
{showSubmitForm && (
<Card className="max-w-2xl mx-auto bg-cream-50">
<CardHeader>
<CardTitle className="font-serif">Recent Letters</CardTitle>
<CardTitle className="font-cormorant text-2xl text-sage-900">Submit Your Question</CardTitle>
<p className="text-sage-700 text-sm">
Pour your heart out to Jane, and she shall guide you with her timeless wisdom.
</p>
</CardHeader>
<CardContent>
<ScrollArea className="h-[500px]">
<div className="space-y-4">
{dearJaneLetters.map((letter) => (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-sage-700 mb-1">
Subject
</label>
<input
type="text"
value={formData.subject}
onChange={(e) => setFormData(prev => ({ ...prev, subject: e.target.value }))}
className="w-full p-3 rounded-lg border-sage-200 focus:ring-sage-500 focus:border-sage-500 bg-white"
placeholder="e.g., Advice on a Delicate Matter"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-sage-700 mb-1">
Your Question
</label>
<textarea
value={formData.question}
onChange={(e) => setFormData(prev => ({ ...prev, question: e.target.value }))}
className="w-full h-32 p-3 rounded-lg border-sage-200 focus:ring-sage-500 focus:border-sage-500 bg-white"
placeholder="Dear Jane..."
required
/>
</div>
<div>
<label className="block text-sm font-medium text-sage-700 mb-1">
Sign As
</label>
<input
type="text"
value={formData.from}
onChange={(e) => setFormData(prev => ({ ...prev, from: e.target.value }))}
className="w-full p-3 rounded-lg border-sage-200 focus:ring-sage-500 focus:border-sage-500 bg-white"
placeholder="e.g., Hopelessly Romantic"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-sage-700 mb-1">
Category
</label>
<select
value={formData.category}
onChange={(e) => setFormData(prev => ({ ...prev, category: e.target.value as DearJaneLetter['category'] }))}
className="w-full p-3 rounded-lg border-sage-200 focus:ring-sage-500 focus:border-sage-500 bg-white"
>
{Object.entries(categoryNames).map(([key, name]) => (
<option key={key} value={key}>{name}</option>
))}
</select>
</div>
<Button type="submit" className="w-full bg-sage-500 hover:bg-sage-600 text-white">
<Mail className="h-4 w-4 mr-2" />
Send Letter to Jane
</Button>
</form>
</CardContent>
</Card>
)}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<Card className="lg:col-span-1 bg-cream-50">
<CardHeader>
<CardTitle className="font-cormorant text-2xl text-sage-900">Browse by Category</CardTitle>
<p className="text-sage-700 text-sm">
{categoryDescriptions[selectedCategory]}
</p>
</CardHeader>
<CardContent>
<Tabs value={selectedCategory} onValueChange={(value) => setSelectedCategory(value as DearJaneLetter['category'])}>
<TabsList className="grid grid-cols-1 gap-2">
{Object.entries(categoryNames).map(([key, name]) => (
<TabsTrigger
key={key}
value={key}
className="w-full flex items-center justify-start gap-2 p-2"
>
{categoryIcons[key as keyof typeof categoryIcons]}
<div className="text-left">
<div className="font-medium">{name}</div>
<div className="text-xs text-sage-600">
{filteredLetters.length} letters
</div>
</div>
</TabsTrigger>
))}
</TabsList>
</Tabs>
<ScrollArea className="h-[400px] mt-4">
<div className="space-y-2">
{filteredLetters.map((letter) => (
<Button
key={letter.id}
variant={selectedLetter.id === letter.id ? 'secondary' : 'ghost'}
className="w-full justify-start"
onClick={() => setSelectedLetter(letter)}
className="w-full justify-start text-left"
onClick={() => setSelectedLetter(letter as DearJaneLetter)}
>
<Mail className="mr-2 h-4 w-4" />
<span className="truncate">{letter.from}</span>
<Mail className="mr-2 h-4 w-4 flex-shrink-0" />
<div className="truncate flex-1">
<div className="font-medium">{letter.subject}</div>
<div className="text-xs text-sage-600 flex items-center gap-1">
<span>From: {letter.from}</span>
{'status' in letter && (
<>
<span></span>
<Clock className="h-3 w-3" />
<span>{letter.status === 'pending' ? 'Awaiting Response' : 'Answered'}</span>
</>
)}
</div>
</div>
</Button>
))}
</div>
@ -41,25 +233,52 @@ const DearJane = () => {
</CardContent>
</Card>
<Card className="lg:col-span-2">
<Card className="lg:col-span-2 bg-cream-50">
<CardHeader>
<CardTitle className="font-serif text-2xl">{selectedLetter.subject}</CardTitle>
<p className="text-sm text-muted-foreground">From: {selectedLetter.from}</p>
<div className="space-y-2">
<CardTitle className="font-cormorant text-2xl text-sage-900">
{selectedLetter.subject}
</CardTitle>
<div className="flex items-center gap-2 text-sm text-sage-600">
<span>From: {selectedLetter.from}</span>
<span></span>
<span>{new Date(selectedLetter.date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}</span>
</div>
</div>
</CardHeader>
<CardContent>
<div className="prose prose-neutral dark:prose-invert">
<div className="prose prose-sage max-w-none">
<div className="mb-8">
<p className="italic">{selectedLetter.question}</p>
<p className="italic text-sage-700">{selectedLetter.question}</p>
</div>
<div>
<p className="font-serif text-lg mb-4">Dear {selectedLetter.from},</p>
<div className="space-y-4">
{selectedLetter.answer.map((paragraph, index) => (
<p key={index} className="mb-4">
<p key={index} className="text-sage-800">
{paragraph}
</p>
))}
<p className="font-serif text-right">Yours truly,<br />Jane Austen</p>
<p className="text-right font-cormorant text-sage-900">
Yours truly,<br />Jane Austen
</p>
</div>
{selectedLetter.relatedBook && (
<div className="mt-8 p-4 bg-sage-50 rounded-lg">
<h3 className="font-cormorant text-xl text-sage-900 mb-2">
From {selectedLetter.relatedBook.title}
</h3>
<blockquote className="italic text-sage-700 border-l-4 border-sage-200 pl-4">
"{selectedLetter.relatedBook.quote}"
<footer className="text-sage-600 mt-2">
{selectedLetter.relatedBook.character}
</footer>
</blockquote>
</div>
)}
</div>
</CardContent>
</Card>
@ -68,4 +287,4 @@ const DearJane = () => {
);
};
export default DearJane;
export default DearJane;

View file

@ -35,6 +35,16 @@ const Home = () => {
</div>
</Link>
{/* Success Stories */}
<Link to="/success-stories" className="feature-card">
<div className="bg-rose-100 p-6 rounded-lg hover:shadow-lg transition">
<h2 className="font-cormorant text-2xl text-sage-900 mb-3">Success Stories</h2>
<p className="text-sage-700">
Real tales of romance from our beloved characters
</p>
</div>
</Link>
{/* Dear Jane */}
<Link to="/advice" className="feature-card">
<div className="bg-cream-100 p-6 rounded-lg hover:shadow-lg transition">
@ -55,16 +65,6 @@ const Home = () => {
</div>
</Link>
{/* Success Stories */}
<Link to="/stories" className="feature-card">
<div className="bg-rose-100 p-6 rounded-lg hover:shadow-lg transition">
<h2 className="font-cormorant text-2xl text-sage-900 mb-3">Success Stories</h2>
<p className="text-sage-700">
Real tales of romance from our beloved characters
</p>
</div>
</Link>
{/* Featured Article */}
<div className="bg-cream-50 p-6 rounded-lg">
<h2 className="font-cormorant text-2xl text-sage-900 mb-3">Latest from Charlotte</h2>

View file

@ -1,43 +1,150 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { successStories } from '@/data/success-stories';
import { useState } from 'react';
import { Heart, MapPin, Calendar } from 'lucide-react';
import { successStories, SuccessStory } from '@/data/success-stories';
import { cn } from '@/lib/utils';
const SuccessStories = () => {
const StoryModal = ({ story, onClose }: { story: SuccessStory; onClose: () => void }) => {
return (
<div className="space-y-8">
<header className="text-center space-y-4">
<h1 className="font-serif text-4xl">Success Stories</h1>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
Tales of matrimonial triumph from the pages of Austen
</p>
</header>
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-lg max-w-4xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6 space-y-6">
<div className="flex justify-between items-start">
<h2 className="font-cormorant text-3xl text-sage-900">{story.couple}</h2>
<button
onClick={onClose}
className="text-sage-500 hover:text-sage-700 transition"
>
<span className="sr-only">Close</span>
×
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl mx-auto">
{successStories.map((story) => (
<Card key={story.id}>
<CardHeader className="flex flex-row items-center gap-4">
<Avatar>
<AvatarImage src={story.image} alt={story.couple} />
<AvatarFallback>{story.couple[0]}</AvatarFallback>
</Avatar>
<img
src={story.imageUrl}
alt={story.couple}
className="w-full h-96 object-cover rounded-lg"
/>
<div className="space-y-6">
<div className="flex flex-wrap justify-center gap-6 text-sage-600">
<div className="flex items-center gap-1">
<Calendar className="h-4 w-4" />
<span>{story.date}</span>
</div>
<div className="flex items-center gap-1">
<MapPin className="h-4 w-4" />
<span>{story.location}</span>
</div>
</div>
<blockquote className="text-center italic text-sage-700 border-l-4 border-sage-200 pl-4">
"{story.quote}"
</blockquote>
<p className="text-sage-700">{story.story}</p>
<div className="grid gap-6">
<div>
<CardTitle className="font-serif">{story.couple}</CardTitle>
<p className="text-sm text-muted-foreground">{story.novel}</p>
<h3 className="font-cormorant text-2xl text-sage-900">The Ceremony</h3>
<p className="text-sage-700">{story.details.ceremony}</p>
</div>
</CardHeader>
<CardContent>
<div className="prose prose-neutral dark:prose-invert">
<blockquote className="italic mb-4">
"{story.quote}"
</blockquote>
<p>{story.story}</p>
<div>
<h3 className="font-cormorant text-2xl text-sage-900">The Reception</h3>
<p className="text-sage-700">{story.details.reception}</p>
</div>
</CardContent>
</Card>
))}
<div>
<h3 className="font-cormorant text-2xl text-sage-900">Special Moments</h3>
<ul className="list-disc list-inside text-sage-700">
{story.details.specialMoments.map((moment, index) => (
<li key={index}>{moment}</li>
))}
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default SuccessStories;
const SuccessStories = () => {
const [selectedStory, setSelectedStory] = useState<SuccessStory | null>(null);
const [expandedStoryId, setExpandedStoryId] = useState<string | null>(null);
const toggleStoryExpansion = (storyId: string) => {
setExpandedStoryId(expandedStoryId === storyId ? null : storyId);
};
return (
<div className="space-y-12">
<header className="text-center space-y-4">
<h1 className="font-cormorant text-4xl text-sage-900">
Love Stories from the Pages of Austen
</h1>
<p className="text-sage-700 max-w-2xl mx-auto">
Discover the romantic journeys of couples who found their perfect match
</p>
</header>
<div className="grid sm:grid-cols-2 lg:grid-cols-2 gap-8">
{successStories.map((story) => (
<article
key={story.id}
className="bg-cream-50 rounded-lg overflow-hidden shadow-lg cursor-pointer hover:shadow-xl transition-shadow"
onClick={() => setSelectedStory(story)}
>
<div className="aspect-w-16 aspect-h-9">
<img
src={story.imageUrl}
alt={story.couple}
className="w-full h-64 object-cover"
/>
</div>
<div className="p-6 space-y-4">
<div className="space-y-2">
<h2 className="font-cormorant text-2xl text-sage-900">
{story.couple}
</h2>
<div className="flex items-center gap-4 text-sm text-sage-600">
<div className="flex items-center gap-1">
<Calendar className="h-4 w-4" />
<span>{story.date}</span>
</div>
<div className="flex items-center gap-1">
<MapPin className="h-4 w-4" />
<span>{story.location}</span>
</div>
</div>
</div>
<p className="text-sage-700 line-clamp-3">{story.story}</p>
<div className="flex justify-between items-center">
<button
onClick={(e) => {
e.stopPropagation();
toggleStoryExpansion(story.id);
}}
className="text-sage-600 hover:text-sage-800 transition"
>
Read More
</button>
<Heart className="h-5 w-5 text-rose-500" />
</div>
</div>
</article>
))}
</div>
{selectedStory && (
<StoryModal
story={selectedStory}
onClose={() => setSelectedStory(null)}
/>
)}
</div>
);
};
export default SuccessStories;

View file

@ -1,51 +1,50 @@
import { useState } from 'react';
import type { VendorListing } from '../types';
import { Search, Filter } from 'lucide-react';
import { VendorListing, VendorCategory } from '@/types/vendors';
import { VENDOR_LISTINGS } from '@/data/vendors';
import VendorCard from '@/components/vendor/VendorCard';
import VendorModal from '@/components/vendor/VendorModal';
import { cn } from '@/lib/utils';
const SAMPLE_VENDORS: VendorListing[] = [
{
id: '1',
name: 'Pemberley Estate',
description: 'A grand estate offering the perfect setting for your matrimonial celebration. With its extensive grounds and elegant halls, Pemberley provides an atmosphere of refined sophistication that would please even the most discerning of couples.',
category: 'venue',
location: 'Derbyshire',
imageUrl: '/images/pemberley.jpg'
},
{
id: '2',
name: 'Mrs. Bennet\'s Matchmaking Services',
description: 'With five daughters successfully married off, Mrs. Bennet brings her expertise to your search for the perfect match. Specializing in gentlemen of good fortune.',
category: 'services',
location: 'Longbourn, Hertfordshire',
imageUrl: '/images/matchmaking.jpg'
},
{
id: '3',
name: 'Modiste Madame Delafield',
description: 'Exquisite wedding attire that combines Regency elegance with modern sensibilities. Our designs have graced the most fashionable assemblies in Bath.',
category: 'attire',
location: 'Bath',
imageUrl: '/images/modiste.jpg'
},
{
id: '4',
name: 'Meryton Assembly Catering',
description: 'From intimate family dinners to grand balls, we provide the finest refreshments worthy of any social occasion. Known for our delectable white soup.',
category: 'catering',
location: 'Meryton',
imageUrl: '/images/catering.jpg'
}
const CATEGORIES: VendorCategory[] = [
'venue',
'services',
'attire',
'catering',
'music',
'flowers',
'transport',
'stationery'
];
const LOCATIONS = Array.from(
new Set(VENDOR_LISTINGS.map(vendor => vendor.location))
).sort();
const PRICE_RANGES = [
{ value: 'all', label: 'All Price Ranges' },
{ value: '£', label: '£ - Budget Friendly' },
{ value: '££', label: '££ - Moderate' },
{ value: '£££', label: '£££ - Premium' },
{ value: '££££', label: '££££ - Luxury' },
{ value: '£££££', label: '£££££ - Ultra Luxury' }
];
const Vendors = () => {
const [selectedCategory, setSelectedCategory] = useState<VendorListing['category'] | 'all'>('all');
const [selectedCategory, setSelectedCategory] = useState<VendorCategory | 'all'>('all');
const [selectedLocation, setSelectedLocation] = useState<string | 'all'>('all');
const [searchQuery, setSearchQuery] = useState('');
const [selectedVendor, setSelectedVendor] = useState<VendorListing | null>(null);
const [priceRange, setPriceRange] = useState<string | 'all'>('all');
const [showFilters, setShowFilters] = useState(false);
const filteredVendors = SAMPLE_VENDORS.filter(vendor => {
const filteredVendors = VENDOR_LISTINGS.filter(vendor => {
const matchesCategory = selectedCategory === 'all' || vendor.category === selectedCategory;
const matchesLocation = selectedLocation === 'all' || vendor.location === selectedLocation;
const matchesPrice = priceRange === 'all' || vendor.priceRange === priceRange;
const matchesSearch = vendor.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
vendor.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
vendor.location.toLowerCase().includes(searchQuery.toLowerCase());
return matchesCategory && matchesSearch;
vendor.description.toLowerCase().includes(searchQuery.toLowerCase());
return matchesCategory && matchesLocation && matchesPrice && matchesSearch;
});
return (
@ -54,66 +53,159 @@ const Vendors = () => {
<header className="text-center space-y-4">
<h1 className="font-cormorant text-4xl text-sage-900">Vendor Directory</h1>
<p className="text-sage-700 max-w-2xl mx-auto">
Discover the finest establishments and services to ensure your special day is nothing short of perfect
Discover the finest establishments and services to ensure your special day is nothing short of perfect.
From grand estates to skilled artisans, find everything you need for a celebration worthy of a Jane Austen novel.
</p>
</header>
{/* Filters */}
<div className="flex flex-col sm:flex-row justify-between gap-4">
<div className="flex gap-2">
{(['all', 'venue', 'services', 'attire', 'catering'] as const).map((category) => (
<button
key={category}
onClick={() => setSelectedCategory(category)}
className={`px-4 py-2 rounded-lg capitalize ${
selectedCategory === category
? 'bg-sage-500 text-white'
: 'bg-sage-100 text-sage-700 hover:bg-sage-200'
}`}
>
{category}
</button>
))}
{/* Search and Filters */}
<div className="space-y-6">
{/* Search Bar and Filter Toggle */}
<div className="relative max-w-2xl mx-auto flex gap-4">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-sage-500" />
<input
type="text"
placeholder="Search vendors..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-3 rounded-lg border border-sage-200 focus:ring-sage-500 focus:border-sage-500"
/>
</div>
<button
onClick={() => setShowFilters(!showFilters)}
className={cn(
"flex items-center gap-2 px-4 py-3 rounded-lg border border-sage-200 hover:bg-sage-50 transition-colors",
showFilters && "bg-sage-50 border-sage-300"
)}
>
<Filter className="h-5 w-5" />
<span>Filters</span>
</button>
</div>
<input
type="text"
placeholder="Search vendors..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="px-4 py-2 rounded-lg border border-sage-200 focus:ring-sage-500 focus:border-sage-500"
/>
{/* Filter Controls */}
{showFilters && (
<div className="max-w-2xl mx-auto bg-cream-50 rounded-lg p-6 space-y-6 border border-sage-200 shadow-sm">
{/* Categories */}
<div className="space-y-3">
<label className="block text-sm font-medium text-sage-700">Category</label>
<div className="flex flex-wrap gap-2">
<button
onClick={() => setSelectedCategory('all')}
className={cn(
"px-4 py-2 rounded-full text-sm transition-colors",
selectedCategory === 'all'
? "bg-sage-500 text-white"
: "bg-white border border-sage-200 text-sage-700 hover:bg-sage-50"
)}
>
All Categories
</button>
{CATEGORIES.map(category => (
<button
key={category}
onClick={() => setSelectedCategory(category)}
className={cn(
"px-4 py-2 rounded-full text-sm capitalize transition-colors",
selectedCategory === category
? "bg-sage-500 text-white"
: "bg-white border border-sage-200 text-sage-700 hover:bg-sage-50"
)}
>
{category}
</button>
))}
</div>
</div>
{/* Locations */}
<div className="space-y-3">
<label className="block text-sm font-medium text-sage-700">Location</label>
<div className="flex flex-wrap gap-2">
<button
onClick={() => setSelectedLocation('all')}
className={cn(
"px-4 py-2 rounded-full text-sm transition-colors",
selectedLocation === 'all'
? "bg-sage-500 text-white"
: "bg-white border border-sage-200 text-sage-700 hover:bg-sage-50"
)}
>
All Locations
</button>
{LOCATIONS.map(location => (
<button
key={location}
onClick={() => setSelectedLocation(location)}
className={cn(
"px-4 py-2 rounded-full text-sm transition-colors",
selectedLocation === location
? "bg-sage-500 text-white"
: "bg-white border border-sage-200 text-sage-700 hover:bg-sage-50"
)}
>
{location}
</button>
))}
</div>
</div>
{/* Price Ranges */}
<div className="space-y-3">
<label className="block text-sm font-medium text-sage-700">Price Range</label>
<div className="flex flex-wrap gap-2">
{PRICE_RANGES.map(({ value, label }) => (
<button
key={value}
onClick={() => setPriceRange(value)}
className={cn(
"px-4 py-2 rounded-full text-sm transition-colors",
priceRange === value
? "bg-sage-500 text-white"
: "bg-white border border-sage-200 text-sage-700 hover:bg-sage-50"
)}
>
{label}
</button>
))}
</div>
</div>
</div>
)}
</div>
{/* Results Count */}
<div className="text-center text-sage-700">
{filteredVendors.length} {filteredVendors.length === 1 ? 'vendor' : 'vendors'} found
</div>
{/* Vendor Grid */}
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-8">
{filteredVendors.map((vendor) => (
<article key={vendor.id} className="bg-cream-50 rounded-lg overflow-hidden shadow-lg">
<div className="aspect-w-16 aspect-h-9 bg-sage-200">
{/* In a real app, this would be a proper image */}
<div className="w-full h-48 bg-sage-300" />
</div>
<div className="p-6 space-y-4">
<div>
<h2 className="font-cormorant text-2xl text-sage-900 mb-2">{vendor.name}</h2>
<p className="text-sage-700 text-sm mb-2">{vendor.location}</p>
<span className="inline-block px-3 py-1 rounded-full text-xs capitalize bg-sage-100 text-sage-700">
{vendor.category}
</span>
</div>
<p className="text-sage-700">{vendor.description}</p>
<button className="w-full bg-sage-500 text-white px-4 py-2 rounded-lg hover:bg-sage-600 transition">
Request Information
</button>
</div>
</article>
<VendorCard
key={vendor.id}
vendor={vendor}
onClick={() => setSelectedVendor(vendor)}
/>
))}
</div>
{/* No Results Message */}
{filteredVendors.length === 0 && (
<div className="text-center py-12 text-sage-700">
No vendors found matching your criteria
<div className="text-center py-12">
<p className="text-sage-700 text-lg">No vendors found matching your criteria</p>
<p className="text-sage-600 mt-2">Try adjusting your filters or search terms</p>
</div>
)}
{/* Vendor Modal */}
{selectedVendor && (
<VendorModal
vendor={selectedVendor}
onClose={() => setSelectedVendor(null)}
/>
)}
</div>
);
};

32
src/types/vendors.ts Normal file
View file

@ -0,0 +1,32 @@
export type VendorCategory =
| 'venue'
| 'services'
| 'attire'
| 'catering'
| 'music'
| 'flowers'
| 'transport'
| 'stationery';
export type VendorRating = {
reputation: number;
elegance: number;
value: number;
};
export type VendorListing = {
id: string;
name: string;
description: string;
category: VendorCategory;
location: string;
imageUrl: string;
priceRange: string;
rating: VendorRating;
features: string[];
contactPerson?: string;
testimonials: Array<{
author: string;
text: string;
}>;
};