Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@
"lucide-react": "^0.468.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.18.2",
"tailwindcss": "^4.3.3"
},
"devDependencies": {
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.7.2",
"vite": "^6.0.1"
"vite": "^6.0.1",
"vite-plugin-svgr": "^5.2.0"
}
}
376 changes: 376 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

99 changes: 5 additions & 94 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,100 +1,11 @@
import { useState } from "react";
import { HomePage } from "./pages/HomePage";
import { LoginPage, SignupPage, TermsPage } from "./pages/AuthPages";
import { MissionRecordPage, PhotoCertificationPage } from "./pages/MissionPages";
import type { BottomNavKey } from "./components/UI";
import { MapPage } from "./pages/MapPage";
import { getMissionById } from "./data/quespot";
import { MissionDetailPage } from "./pages/MissionDetailPage";
import { MyPage } from "./pages/MyPage";
import { QuestyCustomizePage } from "./pages/QuestyCustomizePage";

type AppScreen = "login" | "signup" | "terms" | "home" | "map" | "missionDetail" | "my" | "questy" | "photo" | "record";
import { RouterProvider } from "react-router-dom";
import { router } from "./routes/router";

function App() {
const [screenStack, setScreenStack] = useState<AppScreen[]>(["login"]);
const [selectedMissionId, setSelectedMissionId] = useState("tea-house");
const activeScreen = screenStack[screenStack.length - 1];

const navigate = (screen: AppScreen) => {
setScreenStack((stack) => (stack[stack.length - 1] === screen ? stack : [...stack, screen]));
};

const resetTo = (screen: AppScreen) => {
setScreenStack([screen]);
};

const goBack = () => {
setScreenStack((stack) => (stack.length > 1 ? stack.slice(0, -1) : stack));
};

const openMissionDetail = (missionId: string) => {
setSelectedMissionId(missionId);
navigate("missionDetail");
};

const handleBottomNav = (key: BottomNavKey) => {
if (key === "mission") {
navigate("photo");
return;
}

if (key === "map") {
navigate("map");
return;
}

if (key === "my") {
navigate("my");
return;
}

if (key === "reward") {
navigate("record");
return;
}

navigate("home");
};

const renderScreen = () => {
switch (activeScreen) {
case "login":
return <LoginPage onLogin={() => resetTo("home")} onSignup={() => navigate("signup")} />;
case "signup":
return <SignupPage />;
case "terms":
return <TermsPage />;
case "home":
return (
<HomePage
onNavigate={handleBottomNav}
onOpenMission={openMissionDetail}
onOpenRecord={() => navigate("record")}
onStartMission={() => navigate("photo")}
/>
);
case "map":
return <MapPage onBack={goBack} onNavigate={handleBottomNav} onStartMission={() => navigate("photo")} />;
case "missionDetail":
return <MissionDetailPage mission={getMissionById(selectedMissionId)} onBack={goBack} onStartMission={() => navigate("photo")} />;
case "my":
return <MyPage onNavigate={handleBottomNav} onOpenQuesty={() => navigate("questy")} />;
case "questy":
return <QuestyCustomizePage onBack={goBack} onNavigate={handleBottomNav} />;
case "photo":
return <PhotoCertificationPage onBack={goBack} />;
case "record":
return <MissionRecordPage onBack={goBack} />;
default:
return <LoginPage onLogin={() => resetTo("home")} />;
}
};

return (
<div className="app-shell mobile-app-shell">
<section className="mobile-stage">{renderScreen()}</section>
</div>
<>
<RouterProvider router={router} />
</>
);
}

Expand Down
4 changes: 4 additions & 0 deletions src/assets/icons/home.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/icons/map.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/assets/icons/mission.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/icons/my.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/icons/reward.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
75 changes: 30 additions & 45 deletions src/components/UI.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
import type { ButtonHTMLAttributes, InputHTMLAttributes, PropsWithChildren, ReactNode } from "react";
import {
Landmark,
MapPin,
Search,
} from "lucide-react";
import homeIcon from "../icons/Icon.svg";
import missionIcon from "../icons/Icon (1).svg";
import mapIcon from "../icons/Icon (2).svg";
import rewardIcon from "../icons/Icon (3).svg";
import myIcon from "../icons/Icon (4).svg";
import type {
ButtonHTMLAttributes,
InputHTMLAttributes,
PropsWithChildren,
ReactNode,
} from "react";
import { Landmark, MapPin, Search } from "lucide-react";

type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: "primary" | "secondary" | "ghost" | "disabled";
icon?: ReactNode;
};

export function Button({ children, className = "", icon, variant = "primary", ...props }: ButtonProps) {
export function Button({
children,
className = "",
icon,
variant = "primary",
...props
}: ButtonProps) {
return (
<button className={`button button-${variant} ${className}`} type="button" {...props}>
<button
className={`button button-${variant} ${className}`}
type="button"
{...props}
>
{icon ? <span className="button-icon">{icon}</span> : null}
<span>{children}</span>
</button>
Expand All @@ -37,11 +43,20 @@ export function TextField({ label, ...props }: TextFieldProps) {
);
}

export function Panel({ children, className = "" }: PropsWithChildren<{ className?: string }>) {
export function Panel({
children,
className = "",
}: PropsWithChildren<{ className?: string }>) {
return <section className={`panel ${className}`}>{children}</section>;
}

export function WireImage({ label = "코스 이미지", tall = false }: { label?: string; tall?: boolean }) {
export function WireImage({
label = "코스 이미지",
tall = false,
}: {
label?: string;
tall?: boolean;
}) {
return (
<div className={`wire-image ${tall ? "wire-image-tall" : ""}`}>
<Landmark className="wire-image-icon" size={20} strokeWidth={2.4} />
Expand All @@ -51,36 +66,6 @@ export function WireImage({ label = "코스 이미지", tall = false }: { label?
);
}

export type BottomNavKey = "home" | "mission" | "map" | "reward" | "my";

const navItems: Array<{ key: BottomNavKey; label: string; icon: string }> = [
{ key: "home", label: "홈", icon: homeIcon },
{ key: "mission", label: "미션", icon: missionIcon },
{ key: "map", label: "지도", icon: mapIcon },
{ key: "reward", label: "보상", icon: rewardIcon },
{ key: "my", label: "MY", icon: myIcon },
];

export function BottomNav({
active = "home",
onSelect,
}: {
active?: BottomNavKey;
onSelect?: (key: BottomNavKey) => void;
}) {

return (
<nav className="bottom-nav" aria-label="앱 하단 메뉴">
{navItems.map(({ icon, key, label }) => (
<button className={key === active ? "is-active" : ""} key={key} onClick={() => onSelect?.(key)} type="button">
<img src={icon} alt="" />
<strong>{label}</strong>
</button>
))}
</nav>
);
}

export function SearchInput({ placeholder }: { placeholder: string }) {
return (
<label className="search-box">
Expand Down
5 changes: 0 additions & 5 deletions src/icons/Icon (1).svg

This file was deleted.

3 changes: 0 additions & 3 deletions src/icons/Icon (2).svg

This file was deleted.

4 changes: 0 additions & 4 deletions src/icons/Icon (3).svg

This file was deleted.

3 changes: 0 additions & 3 deletions src/icons/Icon (4).svg

This file was deleted.

4 changes: 0 additions & 4 deletions src/icons/Icon.svg

This file was deleted.

4 changes: 4 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
@import "tailwindcss";

@theme {
--color-primary: #5bb5f8;
}
80 changes: 80 additions & 0 deletions src/layouts/BottomNavigationLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { NavLink, Outlet } from "react-router-dom";
import { PATH } from "../routes/paths";

import HomeIcon from "@/assets/icons/home.svg?react";
import MissionIcon from "@/assets/icons/mission.svg?react";
import MapIcon from "@/assets/icons/map.svg?react";
import RewardIcon from "@/assets/icons/reward.svg?react";
import MyIcon from "@/assets/icons/my.svg?react";

const NAV_ITEMS = [
{
label: "홈",
path: PATH.HOME,
icon: HomeIcon,
},
{
label: "미션",
path: PATH.MISSIONS,
icon: MissionIcon,
},
{
label: "지도",
path: PATH.MAP,
icon: MapIcon,
},
{
label: "보상",
path: PATH.REWARDS,
icon: RewardIcon,
},
{
label: "MY",
path: PATH.MY,
icon: MyIcon,
},
];

export default function BottomNavigationLayout() {
return (
<>
<main className="pb-20">
<Outlet />
</main>

<BottomNavigation />
</>
);
}
const BottomNavigation = () => {
return (
<nav className="fixed bottom-0 left-1/2 z-50 w-full max-w-[430px] -translate-x-1/2 bg-white shadow-[0_-4px_8px_rgba(0,0,0,0.08)]">
<ul className="flex h-[80px] items-center">
{NAV_ITEMS.map(({ label, path, icon: Icon }) => (
<li key={path} className="flex-1">
<NavLink
to={path}
className={({ isActive }) =>
`relative flex flex-col items-center gap-1.5 ${
isActive ? "text-primary" : "text-[#C8E8FF]"
}`
}
>
{({ isActive }) => (
<>
<Icon className="h-7 w-7" />

<span className="text-[9px] font-bold">{label}</span>

{isActive && (
<span className="absolute -bottom-3 h-[3px] w-6 rounded-full bg-primary" />
)}
</>
)}
</NavLink>
</li>
))}
</ul>
</nav>
);
};
11 changes: 11 additions & 0 deletions src/layouts/MobileLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Outlet } from "react-router-dom";

export default function MobileLayout() {
return (
<div className="flex min-h-[100dvh] justify-center bg-[linear-gradient(143deg,#c8e8ff_0%,#eaf5ff_50%,#d4ecff_100%)] border-r border-[rgba(221,228,238,0.8)]">
<div className="relative min-h-dvh w-full max-w-[430px] bg-white shadow-md">
<Outlet />
</div>
</div>
);
}
1 change: 1 addition & 0 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./styles/global.css";
import "./index.css";

ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
Expand Down
Loading
Loading