mirror of
https://github.com/harivansh-afk/Austens-Wedding-Guide.git
synced 2026-04-16 03:01:59 +00:00
improved all pages and changed content to match syllabus
This commit is contained in:
parent
43bd803759
commit
b9eb7cf738
32 changed files with 1290 additions and 300 deletions
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
56
src/components/ui/use-toast.ts
Normal file
56
src/components/ui/use-toast.ts
Normal 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
83
src/components/vendor/VendorCard.tsx
vendored
Normal 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
145
src/components/vendor/VendorModal.tsx
vendored
Normal 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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue