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

@ -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;