Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
eac1f0d
[#20]Feat: 에스크로 결제 API/hooks 확장
BaeSeong-min May 11, 2026
25dc070
[#20]Feat: PaymentDetail 페이지 구현
BaeSeong-min May 11, 2026
a009d85
[#20]Refactor: PaymentsPage를 payment 디렉토리로 이동 및 PageHero 적용
BaeSeong-min May 11, 2026
216fb53
[#20]Fix: 결제 페이지 히어로 타이틀 한국어 번역 추가
BaeSeong-min May 11, 2026
7bbd6de
[#20]Refactor: MyInfo에서 소개/제품/웹사이트 입력 필드 제거
BaeSeong-min May 12, 2026
96ca172
[#20]Fix: SegmentedControl 활성 탭 외곽선 제거
BaeSeong-min May 12, 2026
28691a6
[#20]Refactor: 에스크로 결제 생성 DTO를 counterpartyWalletAddress로 단일화
BaeSeong-min May 12, 2026
d5726f5
[#20]Feat: PaymentDetail 완성 - 에스크로 결제 모달 / 결제 개시 / 토스트 알림 / i18n
BaeSeong-min May 12, 2026
8b76832
[#20]Feat: 결제 목록 진행률 바 + 진행중/종료(ongoing/done) 탭 전환
BaeSeong-min May 12, 2026
0c3e32c
[#20]Refactor: EscrowPaymentModal → 범용 ConfirmModal로 일반화
BaeSeong-min May 12, 2026
734b09c
[#20]Feat: 에스크로 이벤트 확정 시 ConfirmModal로 한 번 더 확인
BaeSeong-min May 12, 2026
546a879
[#20]Feat: 랜딩 페이지 hero에 마운트 애니메이션 추가
BaeSeong-min May 12, 2026
024ce83
[#20]Feat: EscrowProgressBar 단계별 fill 애니메이션 추가
BaeSeong-min May 12, 2026
c50c883
[#20]Feat: PaymentDetail 총 금액 count-up 애니메이션
BaeSeong-min May 12, 2026
5434ce4
[#20]Feat: 결제 목록 row stagger + 우측 detail 패널 slide-in
BaeSeong-min May 12, 2026
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
2 changes: 2 additions & 0 deletions src/apis/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,7 @@ export const ENDPOINTS = {
pay: (id: string) => `/escrow-payments/${id}/pay`,
userByWallet: (address: string) =>
`/escrow-payments/users/wallet/${address}`,
approveEvent: (id: string, escrowId: string, type: string) =>
`/escrow-payments/${id}/escrows/${escrowId}/events/${encodeURIComponent(type)}/approve`,
},
};
25 changes: 21 additions & 4 deletions src/apis/modules/escrowPayments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ export interface EscrowItemDto {
}

export interface CreateEscrowPaymentDto {
buyerId: string;
sellerWalletAddress: string;
counterpartyWalletAddress: string;
memo?: string;
currency?: "XRP" | "RLUSD";
escrows: EscrowItemDto[];
Expand All @@ -20,11 +19,29 @@ export const escrowPaymentsApi = {
create: (data: CreateEscrowPaymentDto) =>
http.post(ENDPOINTS.escrowPayments.root, data),

getList: (
params: {
group?: "ongoing" | "done";
status?:
| "PENDING_APPROVAL"
| "APPROVED"
| "PROCESSING"
| "ACTIVE"
| "COMPLETED"
| "CANCELLED";
page?: number;
limit?: number;
} = {},
) => http.get(ENDPOINTS.escrowPayments.root, { params }),

getById: (id: string) =>
http.get(ENDPOINTS.escrowPayments.byId(id)),

approve: (id: string) =>
http.post(ENDPOINTS.escrowPayments.approve(id)),
approve: (id: string, action?: "ACCEPT" | "REJECT") =>
http.post(ENDPOINTS.escrowPayments.approve(id), action ? { action } : undefined),

approveEvent: (id: string, escrowId: string, type: string) =>
http.post(ENDPOINTS.escrowPayments.approveEvent(id, escrowId, type)),

pay: (id: string) =>
http.post(ENDPOINTS.escrowPayments.pay(id)),
Expand Down
153 changes: 153 additions & 0 deletions src/components/ConfirmModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { useEffect } from "react";

export default function ConfirmModal({
open,
onClose,
onConfirm,
isPending,
title,
subtitle,
cancelLabel,
confirmLabel,
pendingLabel,
}) {
useEffect(() => {
function onKey(e) {
if (e.key === "Escape" && !isPending) onClose?.();
}
if (open) document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, [open, onClose, isPending]);

if (!open) return null;

return (
<div
style={{
position: "fixed",
inset: 0,
background: "rgba(0,0,0,.4)",
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 50,
}}
onClick={isPending ? undefined : onClose}
>
<div
onClick={(e) => e.stopPropagation()}
style={{
width: 411,
background: "#fafafa",
borderRadius: 32,
padding: 20,
display: "flex",
flexDirection: "column",
gap: 60,
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 32,
}}
>
<div
style={{
width: 32,
height: 32,
borderRadius: 9999,
background: "#f5b81f",
color: "#fafafa",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: 20,
fontWeight: 700,
lineHeight: 1,
}}
aria-hidden="true"
>
!
</div>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 24,
textAlign: "center",
}}
>
<span
style={{
fontSize: 24,
fontWeight: 700,
color: "#080616",
lineHeight: "30px",
}}
>
{title}
</span>
{subtitle && (
<span
style={{
fontSize: 16,
fontWeight: 400,
color: "#080616",
lineHeight: "22px",
whiteSpace: "pre-wrap",
}}
>
{subtitle}
</span>
)}
</div>
</div>

<div style={{ display: "flex", gap: 12 }}>
<button
onClick={isPending ? undefined : onClose}
disabled={isPending}
style={{
flex: 1,
padding: 12,
borderRadius: 8,
border: "none",
background: "#080616",
color: "#fafafa",
fontSize: 16,
fontWeight: 700,
lineHeight: "22px",
cursor: isPending ? "default" : "pointer",
opacity: isPending ? 0.6 : 1,
}}
>
{cancelLabel}
</button>
<button
onClick={isPending ? undefined : onConfirm}
disabled={isPending}
style={{
flex: 1,
padding: 12,
borderRadius: 8,
border: "none",
background: "#0056ee",
color: "#fafafa",
fontSize: 16,
fontWeight: 700,
lineHeight: "22px",
cursor: isPending ? "default" : "pointer",
opacity: isPending ? 0.6 : 1,
}}
>
{isPending ? pendingLabel : confirmLabel}
</button>
</div>
</div>
</div>
);
}
2 changes: 1 addition & 1 deletion src/components/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import logoTypo from "@/assets/logo_typo.png";

const navItems = [
{ to: "/matches", key: "nav_partner_matching" },
{ to: "/payments/create", key: "nav_xrp_payment" },
{ to: "/payments", key: "nav_xrp_payment" },
{ to: "/my-business", key: "nav_my_business" },
{ to: "/schedule", key: "nav_schedule" },
];
Expand Down
2 changes: 1 addition & 1 deletion src/components/SegmentedControl.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default function SegmentedControl({ tabs, value, onChange }) {
inset: 0,
borderRadius: 8,
background: "#fafafa",
border: "1px solid #dadada",
border: "none",
zIndex: -1,
}}
transition={{ type: "spring", stiffness: 400, damping: 30 }}
Expand Down
145 changes: 145 additions & 0 deletions src/components/payment/EscrowProgressBar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { motion } from "framer-motion";

const MAX_STEPS = 3;
const ACTIVE_COLOR = "#0056ee";
const PENDING_COLOR = "#dadada";
const ACTIVE_LABEL_COLOR = "#080616";
const EASE = [0.16, 1, 0.3, 1];
const STEP_DELAY = 0.18;

export default function EscrowProgressBar({
steps,
currentStep = 0,
labels = [],
}) {
const total = Math.min(Math.max(steps ?? 0, 1), MAX_STEPS);

return (
<div
style={{
display: "flex",
alignItems: "flex-start",
justifyContent: "center",
width: "100%",
}}
>
{Array.from({ length: total }).map((_, i) => {
const stepNum = i + 1;
const isActive = stepNum <= currentStep;
const isLastStep = i === total - 1;
const nextIsActive = stepNum < currentStep;

return (
<div
key={stepNum}
style={{
display: "flex",
alignItems: "flex-start",
flex: isLastStep ? "0 0 auto" : "1 1 0",
minWidth: 0,
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 4,
width: 43,
flexShrink: 0,
}}
>
<motion.div
initial={{ scale: 0.5, backgroundColor: PENDING_COLOR }}
animate={{
scale: 1,
backgroundColor: isActive ? ACTIVE_COLOR : PENDING_COLOR,
}}
transition={{
duration: 0.4,
delay: i * STEP_DELAY,
ease: EASE,
}}
style={{
width: 20,
height: 20,
borderRadius: 9999,
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: 10,
fontWeight: 500,
lineHeight: 1,
color: "#fafafa",
}}
>
{stepNum}
</motion.div>
<motion.span
initial={{ opacity: 0, color: PENDING_COLOR }}
animate={{
opacity: 1,
color: isActive ? ACTIVE_LABEL_COLOR : PENDING_COLOR,
}}
transition={{
duration: 0.35,
delay: i * STEP_DELAY + 0.1,
ease: EASE,
}}
style={{
fontSize: 10,
fontWeight: 500,
lineHeight: "12px",
textAlign: "center",
maxWidth: 80,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{labels[i] ?? `Step ${stepNum}`}
</motion.span>
</div>

{!isLastStep && (
<div
style={{
flex: 1,
height: 1,
marginTop: 9.5,
position: "relative",
}}
>
<div
style={{
position: "absolute",
inset: 0,
borderTop: `1px dashed ${PENDING_COLOR}`,
}}
/>
<motion.div
initial={{ clipPath: "inset(0 100% 0 0)" }}
animate={{
clipPath: nextIsActive
? "inset(0 0% 0 0)"
: "inset(0 100% 0 0)",
}}
transition={{
duration: 0.5,
delay: i * STEP_DELAY + 0.12,
ease: EASE,
}}
style={{
position: "absolute",
inset: 0,
borderTop: `1px dashed ${ACTIVE_COLOR}`,
}}
/>
</div>
)}
</div>
);
})}
</div>
);
}
Loading
Loading