mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-15 18:14:44 +00:00
Add brewery header, your-rating, beer list, and comments components
This commit is contained in:
@@ -0,0 +1,42 @@
|
|||||||
|
import { Link } from 'react-router';
|
||||||
|
import StarRating from './StarRating';
|
||||||
|
import type { FillerBeer } from '../utils/filler-brewery-detail';
|
||||||
|
|
||||||
|
interface BeerListCardProps {
|
||||||
|
beers: FillerBeer[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const BeerListCard = ({ beers }: BeerListCardProps) => (
|
||||||
|
<div className="card bg-base-100 shadow">
|
||||||
|
<div className="card-body gap-3 p-6">
|
||||||
|
<div className="flex items-baseline justify-between gap-4">
|
||||||
|
<h3 className="text-xl m-0">Beers ({beers.length})</h3>
|
||||||
|
<Link to="/beers" className="link link-primary text-sm">
|
||||||
|
View all beers
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{beers.map((beer) => (
|
||||||
|
<div
|
||||||
|
key={beer.id}
|
||||||
|
className="grid grid-cols-[1fr_auto_auto_auto] items-center gap-3 py-2.5 border-t border-base-content/10 first:border-t-0"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-0.5 min-w-0">
|
||||||
|
<span className="font-bold text-sm">{beer.name}</span>
|
||||||
|
<span className="text-xs text-base-content/60">{beer.description}</span>
|
||||||
|
</div>
|
||||||
|
<span className="badge badge-ghost bg-base-300 text-xs font-semibold whitespace-nowrap">
|
||||||
|
{beer.style}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-base-content/60 tabular-nums whitespace-nowrap">
|
||||||
|
{beer.abv.toFixed(1)}% ABV
|
||||||
|
</span>
|
||||||
|
<StarRating value={beer.rating} size="xs" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default BeerListCard;
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { Heart, HeartSolid, MapPin } from 'iconoir-react';
|
||||||
|
import StarRating from './StarRating';
|
||||||
|
import { formatBreweryAddress } from '../utils/format-address';
|
||||||
|
import type { Brewery } from '../breweries.server';
|
||||||
|
|
||||||
|
interface BreweryHeaderCardProps {
|
||||||
|
brewery: Brewery;
|
||||||
|
foundedYear: number;
|
||||||
|
liked: boolean;
|
||||||
|
likeCount: number;
|
||||||
|
avgRating: number;
|
||||||
|
ratingsCount: number;
|
||||||
|
onToggleLike: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BreweryHeaderCard = ({
|
||||||
|
brewery,
|
||||||
|
foundedYear,
|
||||||
|
liked,
|
||||||
|
likeCount,
|
||||||
|
avgRating,
|
||||||
|
ratingsCount,
|
||||||
|
onToggleLike,
|
||||||
|
}: BreweryHeaderCardProps) => (
|
||||||
|
<div className="card bg-base-100 shadow">
|
||||||
|
<div className="card-body gap-4 p-7">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<span className="text-xs font-bold uppercase tracking-widest text-base-content/50">
|
||||||
|
Brewery
|
||||||
|
</span>
|
||||||
|
<span className="badge bg-[var(--color-highlight)] text-[var(--color-highlight-content)]">
|
||||||
|
Est. {foundedYear}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl leading-tight m-0">{brewery.breweryName}</h1>
|
||||||
|
{brewery.location && (
|
||||||
|
<p className="text-sm text-base-content/60 flex items-center gap-1 m-0">
|
||||||
|
<MapPin className="size-4" aria-hidden="true" />
|
||||||
|
{formatBreweryAddress(brewery.location)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3 flex-wrap">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onToggleLike}
|
||||||
|
className={`btn btn-sm ${liked ? 'btn-primary' : 'btn-outline'}`}
|
||||||
|
>
|
||||||
|
{liked ? (
|
||||||
|
<HeartSolid className="size-4" aria-hidden="true" />
|
||||||
|
) : (
|
||||||
|
<Heart className="size-4" aria-hidden="true" />
|
||||||
|
)}
|
||||||
|
{liked ? 'Liked' : 'Like'} · {likeCount}
|
||||||
|
</button>
|
||||||
|
<div className="flex items-baseline gap-1.5">
|
||||||
|
<StarRating value={avgRating} size="sm" />
|
||||||
|
<span className="font-bold">{avgRating.toFixed(1)}</span>
|
||||||
|
<span className="text-sm text-base-content/60">({ratingsCount} ratings)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-base-content/70 leading-relaxed text-pretty m-0">
|
||||||
|
{brewery.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default BreweryHeaderCard;
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { Heart, HeartSolid } from 'iconoir-react';
|
||||||
|
import StarRating from './StarRating';
|
||||||
|
import type { FillerComment } from '../utils/filler-brewery-detail';
|
||||||
|
|
||||||
|
interface CommentItemProps {
|
||||||
|
comment: FillerComment;
|
||||||
|
onToggleLike: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CommentItem = ({ comment, onToggleLike }: CommentItemProps) => {
|
||||||
|
const likeCount = comment.likes + (comment.liked ? 1 : 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-3 items-start border-t border-base-content/10 pt-4">
|
||||||
|
<div className="avatar avatar-placeholder shrink-0">
|
||||||
|
<div className="bg-secondary text-secondary-content w-9 rounded-full">
|
||||||
|
<span className="text-sm font-bold">{comment.initials}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 flex flex-col gap-1 min-w-0">
|
||||||
|
<div className="flex items-baseline gap-2 flex-wrap">
|
||||||
|
<span className="font-bold text-sm">{comment.user}</span>
|
||||||
|
<StarRating value={comment.rating} size="xs" />
|
||||||
|
<span className="text-xs text-base-content/60">{comment.time}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm leading-relaxed m-0">{comment.text}</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onToggleLike(comment.id)}
|
||||||
|
className={`self-start inline-flex items-center gap-1 text-xs font-semibold ${
|
||||||
|
comment.liked ? 'text-primary' : 'text-base-content/60'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{comment.liked ? (
|
||||||
|
<HeartSolid className="size-3.5" aria-hidden="true" />
|
||||||
|
) : (
|
||||||
|
<Heart className="size-3.5" aria-hidden="true" />
|
||||||
|
)}
|
||||||
|
{likeCount} {likeCount === 1 ? 'like' : 'likes'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CommentItem;
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import CommentItem from './CommentItem';
|
||||||
|
import type { FillerComment } from '../utils/filler-brewery-detail';
|
||||||
|
|
||||||
|
interface CommentsCardProps {
|
||||||
|
comments: FillerComment[];
|
||||||
|
loggedIn: boolean;
|
||||||
|
currentUserInitials: string;
|
||||||
|
onAddComment: (text: string) => void;
|
||||||
|
onToggleCommentLike: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CommentsCard = ({
|
||||||
|
comments,
|
||||||
|
loggedIn,
|
||||||
|
currentUserInitials,
|
||||||
|
onAddComment,
|
||||||
|
onToggleCommentLike,
|
||||||
|
}: CommentsCardProps) => {
|
||||||
|
const [commentText, setCommentText] = useState('');
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
const text = commentText.trim();
|
||||||
|
if (!text) return;
|
||||||
|
onAddComment(text);
|
||||||
|
setCommentText('');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card bg-base-100 shadow">
|
||||||
|
<div className="card-body gap-5 p-6">
|
||||||
|
<h3 className="text-xl m-0">Comments ({comments.length})</h3>
|
||||||
|
|
||||||
|
{loggedIn ? (
|
||||||
|
<div className="flex gap-3 items-start">
|
||||||
|
<div className="avatar avatar-placeholder shrink-0">
|
||||||
|
<div className="bg-secondary text-secondary-content w-9 rounded-full">
|
||||||
|
<span className="text-sm font-bold">{currentUserInitials}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 flex flex-col gap-2">
|
||||||
|
<textarea
|
||||||
|
rows={3}
|
||||||
|
placeholder="Share your thoughts on this brewery"
|
||||||
|
value={commentText}
|
||||||
|
onChange={(e) => setCommentText(e.target.value)}
|
||||||
|
className="textarea w-full"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={!commentText.trim()}
|
||||||
|
className="btn btn-sm btn-primary self-end"
|
||||||
|
>
|
||||||
|
Post comment
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div role="alert" className="alert alert-info alert-soft">
|
||||||
|
Sign in to like, rate, and comment on breweries.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{comments.map((comment) => (
|
||||||
|
<CommentItem
|
||||||
|
key={comment.id}
|
||||||
|
comment={comment}
|
||||||
|
onToggleLike={onToggleCommentLike}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CommentsCard;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import StarRating from './StarRating';
|
||||||
|
|
||||||
|
interface YourRatingCardProps {
|
||||||
|
value: number;
|
||||||
|
onChange: (value: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const YourRatingCard = ({ value, onChange }: YourRatingCardProps) => (
|
||||||
|
<div className="card bg-base-100 shadow">
|
||||||
|
<div className="card-body flex-row flex-wrap items-center justify-between gap-4 p-6">
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<h3 className="text-xl m-0">Your rating</h3>
|
||||||
|
<p className="text-sm text-base-content/60 m-0">
|
||||||
|
{value
|
||||||
|
? `You rated this ${value} of 5. Tap the same star to clear.`
|
||||||
|
: 'Tap a star to rate this brewery.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<StarRating value={value} onChange={onChange} size="lg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default YourRatingCard;
|
||||||
Reference in New Issue
Block a user