diff --git a/src/components/common/SectionHeader.tsx b/src/components/common/SectionHeader.tsx new file mode 100644 index 0000000..c891bf7 --- /dev/null +++ b/src/components/common/SectionHeader.tsx @@ -0,0 +1,36 @@ +import { ChevronRight } from "lucide-react"; +import type { ReactNode } from "react"; + +type SectionHeaderProps = { + title: string; + icon?: ReactNode; + actionLabel?: string; + onActionClick?: () => void; +}; + +export default function SectionHeader({ + title, + icon, + actionLabel, + onActionClick, +}: SectionHeaderProps) { + return ( +
+

+ {icon} + {title} +

+ + {actionLabel ? ( + + ) : null} +
+ ); +} \ No newline at end of file diff --git a/src/components/home/ActivitySummary.tsx b/src/components/home/ActivitySummary.tsx new file mode 100644 index 0000000..b43cf0e --- /dev/null +++ b/src/components/home/ActivitySummary.tsx @@ -0,0 +1,31 @@ +type ActivitySummaryItem = { + value: string; + label: string; +}; + +type ActivitySummaryProps = { + items: ActivitySummaryItem[]; +}; + +export default function ActivitySummary({ items }: ActivitySummaryProps) { + return ( +
+ {items.map((item) => ( +
+ + {item.value} + + + {item.label} + +
+ ))} +
+ ); +} \ No newline at end of file diff --git a/src/components/home/BonusBanner.tsx b/src/components/home/BonusBanner.tsx new file mode 100644 index 0000000..8844864 --- /dev/null +++ b/src/components/home/BonusBanner.tsx @@ -0,0 +1,27 @@ +type BonusBannerProps = { + mascotSrc: string; +}; + +export default function BonusBanner({ mascotSrc }: BonusBannerProps) { + return ( +
+ Quespot 캐릭터 + +
+ + 소도시 미션에서 +
+ 추가 보너스 포인트! +
+ +

+ 인구감소지역 미션 완료 시 +20% 보너스 +

+
+
+ ); +} \ No newline at end of file diff --git a/src/components/home/CategoryGrid.tsx b/src/components/home/CategoryGrid.tsx new file mode 100644 index 0000000..3a619ea --- /dev/null +++ b/src/components/home/CategoryGrid.tsx @@ -0,0 +1,79 @@ +import { + Building2, + Camera, + Compass, + Leaf, + Palette, + ShoppingBag, + Sunrise, + Utensils, + Waves, + type LucideIcon, +} from "lucide-react"; +import SectionHeader from "@/components/common/SectionHeader"; + +const categoryToneClasses: Record = { + violet: "bg-[#eee1ff] text-[#8b5cf6]", + green: "bg-[#e1faef] text-green-600", + amber: "bg-[#fff1c9] text-amber-600", + blue: "bg-[#dcecff] text-[#2577ff]", + pink: "bg-[#ffe1f0] text-pink-500", + cyan: "bg-[#cff9fb] text-cyan-600", + rose: "bg-[#ffdede] text-red-500", + purple: "bg-[#f0e3ff] text-purple-600", +}; + +const categoryIcons: Record = { + history: Building2, + nature: Leaf, + food: Utensils, + night: Sunrise, + photo: Camera, + activity: Waves, + shopping: ShoppingBag, + art: Palette, +}; + +type Category = { + id: string; + label: string; + tone: string; +}; + +type CategoryGridProps = { + categories: Category[]; +}; + +export default function CategoryGrid({ categories }: CategoryGridProps) { + return ( +
+ + +
+ {categories.map((category) => { + const Icon = categoryIcons[category.id] ?? Compass; + const toneClass = + categoryToneClasses[category.tone] ?? categoryToneClasses.blue; + + return ( + + ); + })} +
+
+ ); +} \ No newline at end of file diff --git a/src/components/home/HomeHeader.tsx b/src/components/home/HomeHeader.tsx new file mode 100644 index 0000000..5e11fa8 --- /dev/null +++ b/src/components/home/HomeHeader.tsx @@ -0,0 +1,43 @@ +import { Bell } from "lucide-react"; + +type HomeHeaderProps = { + mascotSrc: string; + notificationCount?: number; + onBellClick?: () => void; +}; + +export default function HomeHeader({ + mascotSrc, + notificationCount = 0, + onBellClick, +}: HomeHeaderProps) { + return ( +
+
+ Quespot 캐릭터 + + Quespot + +
+ + +
+ ); +} \ No newline at end of file diff --git a/src/components/home/HomeHero.tsx b/src/components/home/HomeHero.tsx new file mode 100644 index 0000000..c8b2666 --- /dev/null +++ b/src/components/home/HomeHero.tsx @@ -0,0 +1,50 @@ +import { ChevronRight } from "lucide-react"; +import { Button } from "@/components/UI"; + +type HomeHeroProps = { + nickname: string; + mascotSrc: string; + onExploreClick: () => void; +}; + +export default function HomeHero({ + nickname, + mascotSrc, + onExploreClick, +}: HomeHeroProps) { + return ( +
+
+ + {nickname}님 안녕하세요! + + +

+ 미션으로 떠나는 +
+ 특별한 여행 +

+ +

+ Questy와 함께 일상을 +
+ 여행으로 바꿔보세요 +

+ + +
+ + Quespot 캐릭터 +
+ ); +} \ No newline at end of file diff --git a/src/components/home/MissionCard.tsx b/src/components/home/MissionCard.tsx new file mode 100644 index 0000000..7d2305e --- /dev/null +++ b/src/components/home/MissionCard.tsx @@ -0,0 +1,69 @@ +import { Heart, MapPin } from "lucide-react"; + +const missionToneClasses: Record = { + cream: "bg-[linear-gradient(145deg,#fff6cc_0%,#fffef6_100%)]", + lavender: "bg-[linear-gradient(145deg,#f1e8ff_0%,#fffaff_100%)]", + mint: "bg-[linear-gradient(145deg,#ddfaee_0%,#fafffd_100%)]", + peach: "bg-[linear-gradient(145deg,#ffe6d7_0%,#fff9f5_100%)]", + blue: "bg-[linear-gradient(145deg,#dff1ff_0%,#f7fcff_100%)]", +}; + +type Mission = { + id: string | number; + title: string; + category: string; + distance: string; + points: number; + visual: string; + tone: string; +}; + +type MissionCardProps = { + mission: Mission; + onClick: () => void; +}; + +export default function MissionCard({ mission, onClick }: MissionCardProps) { + const toneClass = missionToneClasses[mission.tone] ?? missionToneClasses.blue; + + return ( +
+
+ + + + {mission.visual} + +
+ +
+ + {mission.category} + + + + {mission.title} + + +
+ + + {mission.distance} + + + +{mission.points}P +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/home/NearbySpotCard.tsx b/src/components/home/NearbySpotCard.tsx new file mode 100644 index 0000000..e6b9495 --- /dev/null +++ b/src/components/home/NearbySpotCard.tsx @@ -0,0 +1,85 @@ +import { ChevronRight, ClipboardCheck, MapPin, Star } from "lucide-react"; + +type NearbySpot = { + id: string | number; + name: string; + distance: string; + done?: boolean; + status?: string; + missionCount?: number; + rating?: number; + badge?: number; +}; + +type NearbySpotCardProps = { + spot: NearbySpot; + onClick: () => void; +}; + +export default function NearbySpotCard({ spot, onClick }: NearbySpotCardProps) { + const isDone = Boolean(spot.done); + + return ( +
+ + + + {isDone ? ( + + ) : null} + + {spot.badge ? ( + + {spot.badge} + + ) : null} + + +
+ + {spot.name} + + +

+ {spot.distance} + + + + {isDone || spot.status === "완료" ? ( + 완료 + ) : ( + `미션 ${spot.missionCount ?? 0}개` + )} +

+
+ + {spot.rating !== undefined ? ( + + + {spot.rating} + + ) : ( + + )} + + +
+ ); +} \ No newline at end of file diff --git a/src/components/home/NearbySpotSection.tsx b/src/components/home/NearbySpotSection.tsx new file mode 100644 index 0000000..741245c --- /dev/null +++ b/src/components/home/NearbySpotSection.tsx @@ -0,0 +1,47 @@ +import { MapPin } from "lucide-react"; +import SectionHeader from "@/components/common/SectionHeader"; +import NearbySpotCard from "@/components/home/NearbySpotCard"; + +type NearbySpot = { + id: string | number; + name: string; + distance: string; + done?: boolean; + status?: string; + missionCount?: number; + rating?: number; + badge?: number; +}; + +type NearbySpotSectionProps = { + spots: NearbySpot[]; + onMapClick: () => void; + onSpotClick: (spot: NearbySpot) => void; +}; + +export default function NearbySpotSection({ + spots, + onMapClick, + onSpotClick, +}: NearbySpotSectionProps) { + return ( +
+ } + actionLabel="지도보기" + onActionClick={onMapClick} + /> + +
+ {spots.map((spot) => ( + onSpotClick(spot)} + /> + ))} +
+
+ ); +} \ No newline at end of file diff --git a/src/components/home/RecommendedMissionSection.tsx b/src/components/home/RecommendedMissionSection.tsx new file mode 100644 index 0000000..8e0a3c9 --- /dev/null +++ b/src/components/home/RecommendedMissionSection.tsx @@ -0,0 +1,46 @@ +import SectionHeader from "@/components/common/SectionHeader"; +import MissionCard from "@/components/home/MissionCard"; + +type Mission = { + id: string | number; + title: string; + category: string; + distance: string; + points: number; + visual: string; + tone: string; +}; + +type RecommendedMissionSectionProps = { + missions: Mission[]; + showAll: boolean; + onToggleShowAll: () => void; + onMissionClick: (mission: Mission) => void; +}; + +export default function RecommendedMissionSection({ + missions, + showAll, + onToggleShowAll, + onMissionClick, +}: RecommendedMissionSectionProps) { + return ( +
+ + +
+ {missions.map((mission) => ( + onMissionClick(mission)} + /> + ))} +
+
+ ); +} \ No newline at end of file diff --git a/src/layouts/BottomNavigationLayout.tsx b/src/layouts/BottomNavigationLayout.tsx index 7e71aae..49f6432 100644 --- a/src/layouts/BottomNavigationLayout.tsx +++ b/src/layouts/BottomNavigationLayout.tsx @@ -38,7 +38,7 @@ const NAV_ITEMS = [ export default function BottomNavigationLayout() { return ( <> -
+
@@ -46,29 +46,41 @@ export default function BottomNavigationLayout() { ); } + const BottomNavigation = () => { return ( -