From 08798cddc04fb7134efc871c1de91477cddebe1d Mon Sep 17 00:00:00 2001 From: seongmin Date: Sat, 9 May 2026 21:47:33 +0900 Subject: [PATCH 01/19] =?UTF-8?q?[#11]Feat:=20=ED=8C=8C=ED=8A=B8=EB=84=88?= =?UTF-8?q?=20=EB=A7=A4=EC=B9=AD=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=9E=88?= =?UTF-8?q?=EC=96=B4=EB=A1=9C=20=EC=84=B9=EC=85=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 2 +- src/lib/i18n/dict.js | 22 +++ src/pages/Matches.jsx | 303 +++++++++--------------------------------- 3 files changed, 83 insertions(+), 244 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 2faf22d..e01e5c6 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -24,7 +24,7 @@ export default function App() {
{ - const raw = Number(searchParams.get("limit") || 5); - return Number.isFinite(raw) ? Math.min(Math.max(raw, 1), 20) : 5; - }); - const [companyFilter, setCompanyFilter] = useState( - searchParams.get("companyId") || "", - ); - const [currency, setCurrency] = useState("XRP"); - const [message, setMessage] = useState(""); - const [submittedParams, setSubmittedParams] = useState(() => { - const buyerId = searchParams.get("buyerId") || storedBuyerId; - return isObjectId(buyerId) ? { buyerId, limit: 5 } : null; - }); - const [creatingPaymentId, setCreatingPaymentId] = useState(""); - const navigate = useNavigate(); - - const { - data: matchesData, - isFetching: loading, - isError, - error, - } = useMatches(submittedParams?.buyerId, submittedParams?.limit, { - enabled: !!submittedParams, - }); - - const matches = useMemo(() => matchesData?.data || [], [matchesData]); - - const filteredMatches = useMemo(() => { - if (!companyFilter) return matches; - return matches.filter( - (item) => item.company && item.company._id === companyFilter, - ); - }, [matches, companyFilter]); - - useEffect(() => { - if ( - companyFilter && - matches.length > 0 && - matches.every((r) => r.company?._id !== companyFilter) - ) { - setMessage("Selected company is not in the latest top results."); - } - }, [matches, companyFilter]); - - const { mutateAsync: executeCreatePayment } = useCreatePayment(); - - function updateParams(nextBuyerId, nextLimit, nextCompanyId) { - const next = new URLSearchParams(); - if (nextBuyerId) next.set("buyerId", nextBuyerId); - if (nextLimit) next.set("limit", String(nextLimit)); - if (nextCompanyId) next.set("companyId", nextCompanyId); - setSearchParams(next); - } - - function onSubmit(e) { - e.preventDefault(); - if (!isObjectId(buyerInput)) { - setMessage("Valid buyerId (24 hex) required."); - return; - } - setMessage(""); - updateParams(buyerInput.trim(), limitInput, companyFilter.trim() || ""); - setSubmittedParams({ buyerId: buyerInput.trim(), limit: limitInput }); - } - - async function handleCreatePayment(companyId) { - if (!submittedParams?.buyerId) return; - setCreatingPaymentId(companyId); - setMessage(""); - try { - const res = await executeCreatePayment({ - payload: { - amount: 1, - currency, - buyerId: submittedParams.buyerId, - companyId, - }, - idemKey: newIdemKey(), - }); - const pid = res?._id; - if (pid) navigate(`/payments/checkout/${pid}`); - } catch (err) { - setMessage(err.message || "Failed to create payment"); - } finally { - setCreatingPaymentId(""); - } - } + const { t } = useI18n(); return ( -
-

Matches

-
-
- setBuyerInput(e.target.value)} - placeholder="Buyer ID (Mongo ObjectId)" - /> - setCompanyFilter(e.target.value)} - placeholder="Optional companyId filter" - /> - - setLimitInput( - Math.min(Math.max(Number(e.target.value) || 1, 1), 20), - ) - } - /> - - + {t("matches_badge")} +
-
-
- -
- - {isError && ( -
- {error?.message || "Failed to load matches"} -
- )} - {message && ( -
- {message} +
+

+ {t("matches_title")} +

+

+ {t("matches_subtitle")} +

- )} - - {!loading && - filteredMatches.length === 0 && - submittedParams && - !isError && ( -
- No matches yet. Try increasing the limit or updating company data. -
- )} - -
- {filteredMatches.map((match) => ( - -
-
- {match.company?.name} -
{match.company?.industry}
-
- - score {Number(match.score).toFixed(2)} - -
- {match.company?.images?.[0]?.url && ( -
- {match.company.name} -
- )} - {(match.reasons || []).length > 0 && ( -
    - {match.reasons.map((reason) => ( -
  • {reason}
  • - ))} -
- )} -
- - {match.company?.videoUrl && ( - - )} - -
-
- ))} -
- -
- Create a new buyer ·{" "} - Browse companies
); From 4ad1add91fcc41a1b35c08018ddf6b56ab7a717b Mon Sep 17 00:00:00 2001 From: seongmin Date: Sat, 9 May 2026 22:02:14 +0900 Subject: [PATCH 02/19] =?UTF-8?q?[#11]Feat:=20AI=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=EC=9D=B8=ED=92=8B=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EB=B0=8F=20/partners/search=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/PartnerSearchInput.jsx | 79 +++++++++++++++++++++++++++ src/components/SquareButton.jsx | 21 +++++-- src/hooks/usePartners.js | 11 ++++ src/pages/Matches.jsx | 21 ++++++- 4 files changed, 127 insertions(+), 5 deletions(-) create mode 100644 src/components/PartnerSearchInput.jsx create mode 100644 src/hooks/usePartners.js diff --git a/src/components/PartnerSearchInput.jsx b/src/components/PartnerSearchInput.jsx new file mode 100644 index 0000000..f5ff4aa --- /dev/null +++ b/src/components/PartnerSearchInput.jsx @@ -0,0 +1,79 @@ +import { useState } from "react"; +import { Search } from "lucide-react"; +import { useI18n } from "@/lib/i18n/I18nProvider"; +import SquareButton from "@/components/SquareButton"; + +export default function PartnerSearchInput({ onSearch, isLoading }) { + const { t } = useI18n(); + const [query, setQuery] = useState(""); + + const handleSearch = () => { + if (!query.trim() || isLoading) return; + onSearch(query.trim()); + }; + + const handleKeyDown = (e) => { + if (e.key === "Enter") handleSearch(); + }; + + return ( +
+
+
+ + setQuery(e.target.value)} + onKeyDown={handleKeyDown} + placeholder={t("matches_search_placeholder")} + style={{ + flex: 1, + border: "none", + outline: "none", + background: "transparent", + fontSize: 16, + color: "#080616", + lineHeight: "24px", + }} + /> +
+ + {t("search_button")} + +
+
+ ); +} diff --git a/src/components/SquareButton.jsx b/src/components/SquareButton.jsx index 18a3dc7..0bc28a8 100644 --- a/src/components/SquareButton.jsx +++ b/src/components/SquareButton.jsx @@ -3,24 +3,37 @@ export default function SquareButton({ onClick, variant = "outline", type = "button", + disabled = false, + style: styleProp, }) { + const bg = + variant === "solid" + ? "#080616" + : variant === "primary" + ? "#0056ee" + : "#fafafa"; + const color = variant === "outline" ? "#080616" : "#fafafa"; + const border = variant === "outline" ? "1px solid #080616" : "none"; + return (
+ +
+ +
); } From 950428ba9f24638dd1c5cacff2ba8770861fbaac Mon Sep 17 00:00:00 2001 From: seongmin Date: Sat, 9 May 2026 22:04:49 +0900 Subject: [PATCH 03/19] =?UTF-8?q?[#11]Feat:=20PartnerTableRow=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/PartnerTableRow.jsx | 145 +++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 src/components/PartnerTableRow.jsx diff --git a/src/components/PartnerTableRow.jsx b/src/components/PartnerTableRow.jsx new file mode 100644 index 0000000..8b1dd42 --- /dev/null +++ b/src/components/PartnerTableRow.jsx @@ -0,0 +1,145 @@ +export default function PartnerTableRow({ + name, + country, + industries, + profile, + avatarUrl, +}) { + const industryText = Array.isArray(industries) + ? industries.join(", ") + : (industries ?? ""); + + return ( +
+
+ +
+
+ {avatarUrl ? ( + {name} + ) : ( +
+ )} + + {name} + +
+ +
+ + {country} + +
+ +
+ + {industryText} + +
+ +
+ + {profile} + +
+
+
+ ); +} From 1054f0796ed4cbe749c0be5c76e019e9eaf9e71f Mon Sep 17 00:00:00 2001 From: seongmin Date: Sat, 9 May 2026 22:22:30 +0900 Subject: [PATCH 04/19] =?UTF-8?q?[#11]Feat:=20=ED=8C=8C=ED=8A=B8=EB=84=88?= =?UTF-8?q?=20=EB=A7=A4=EC=B9=AD=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=20=EB=B0=8F=20=EA=B2=80=EC=83=89=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20UI=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/usePartners.js | 2 +- src/lib/i18n/dict.js | 4 ++ src/pages/Matches.jsx | 125 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 128 insertions(+), 3 deletions(-) diff --git a/src/hooks/usePartners.js b/src/hooks/usePartners.js index 88f8f21..f9f8adf 100644 --- a/src/hooks/usePartners.js +++ b/src/hooks/usePartners.js @@ -5,7 +5,7 @@ export function usePartnerSearch(q, options = {}) { return useQuery({ queryKey: ["partners", "search", q], queryFn: () => partnersApi.search({ q }), - enabled: q, + enabled: !!q, ...options, }); } diff --git a/src/lib/i18n/dict.js b/src/lib/i18n/dict.js index a4a19d1..fd7215f 100644 --- a/src/lib/i18n/dict.js +++ b/src/lib/i18n/dict.js @@ -530,6 +530,8 @@ Object.assign(en, { matches_col_country: "Country", matches_col_industries: "Industries", matches_col_profile: "Company Profile", + matches_empty_prompt: "Enter a keyword to find partners", + matches_no_results: "No results found.", }); const ko = { @@ -792,6 +794,8 @@ const ko = { matches_col_country: "국가", matches_col_industries: "산업", matches_col_profile: "업체 소개", + matches_empty_prompt: "검색어를 입력하여 파트너를 찾아보세요", + matches_no_results: "검색 결과가 없습니다.", }; export const dict = { diff --git a/src/pages/Matches.jsx b/src/pages/Matches.jsx index b71deaa..edbc775 100644 --- a/src/pages/Matches.jsx +++ b/src/pages/Matches.jsx @@ -1,13 +1,24 @@ import { useState } from "react"; +import { Plus } from "lucide-react"; import { useI18n } from "@/lib/i18n/I18nProvider"; import PartnerSearchInput from "@/components/PartnerSearchInput"; +import PartnerTableRow from "@/components/PartnerTableRow"; +import SquareButton from "@/components/SquareButton"; import { usePartnerSearch } from "@/hooks/usePartners"; +const TABLE_HEADERS = [ + { key: "company", width: 417 }, + { key: "country", width: 180 }, + { key: "industries", width: 197 }, + { key: "profile", width: null }, +]; + export default function Matches() { const { t } = useI18n(); const [submittedQuery, setSubmittedQuery] = useState(""); - const { isFetching } = usePartnerSearch(submittedQuery); + const { data, isFetching } = usePartnerSearch(submittedQuery); + const partners = data?.data ?? []; return (
@@ -78,7 +89,7 @@ export default function Matches() { style={{ maxWidth: 1440, margin: "0 auto", - padding: "40px 80px 80px", + padding: "40px 80px 0", }} >
+ +
+
+ + + {t("matches_add_partner")} + +
+ +
+
+
+
+ {TABLE_HEADERS.map(({ key, width }) => ( +
+ + {t(`matches_col_${key}`)} + +
+ ))} +
+
+ + {partners.map((partner, i) => ( + + ))} + + {!submittedQuery && ( +
+ {t("matches_empty_prompt")} +
+ )} + + {!isFetching && submittedQuery && partners.length === 0 && ( +
+ {t("matches_no_results")} +
+ )} +
+
); } From ce6f1841ca4826905e2d08b24b59eb03f1654c15 Mon Sep 17 00:00:00 2001 From: seongmin Date: Sat, 9 May 2026 22:26:56 +0900 Subject: [PATCH 05/19] =?UTF-8?q?[#11]Refactor:=20=ED=8C=8C=ED=8A=B8?= =?UTF-8?q?=EB=84=88=20=ED=85=8C=EC=9D=B4=EB=B8=94=EC=9D=84=20PartnerTable?= =?UTF-8?q?=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A1=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/PartnerTable.jsx | 92 +++++++++++++++++++++++++++++++++ src/pages/Matches.jsx | 88 +++---------------------------- 2 files changed, 98 insertions(+), 82 deletions(-) create mode 100644 src/components/PartnerTable.jsx diff --git a/src/components/PartnerTable.jsx b/src/components/PartnerTable.jsx new file mode 100644 index 0000000..071eaeb --- /dev/null +++ b/src/components/PartnerTable.jsx @@ -0,0 +1,92 @@ +import { useI18n } from "@/lib/i18n/I18nProvider"; +import PartnerTableRow from "@/components/PartnerTableRow"; + +const TABLE_HEADERS = [ + { key: "company", width: 417 }, + { key: "country", width: 180 }, + { key: "industries", width: 197 }, + { key: "profile", width: null }, +]; + +export default function PartnerTable({ partners, isFetching, submittedQuery }) { + const { t } = useI18n(); + + return ( +
+
+
+
+ {TABLE_HEADERS.map(({ key, width }) => ( +
+ + {t(`matches_col_${key}`)} + +
+ ))} +
+
+ + {partners.map((partner, i) => ( + + ))} + + {!submittedQuery && ( +
+ {t("matches_empty_prompt")} +
+ )} + + {!isFetching && submittedQuery && partners.length === 0 && ( +
+ {t("matches_no_results")} +
+ )} +
+ ); +} diff --git a/src/pages/Matches.jsx b/src/pages/Matches.jsx index edbc775..1eef78b 100644 --- a/src/pages/Matches.jsx +++ b/src/pages/Matches.jsx @@ -2,17 +2,10 @@ import { useState } from "react"; import { Plus } from "lucide-react"; import { useI18n } from "@/lib/i18n/I18nProvider"; import PartnerSearchInput from "@/components/PartnerSearchInput"; -import PartnerTableRow from "@/components/PartnerTableRow"; +import PartnerTable from "@/components/PartnerTable"; import SquareButton from "@/components/SquareButton"; import { usePartnerSearch } from "@/hooks/usePartners"; -const TABLE_HEADERS = [ - { key: "company", width: 417 }, - { key: "country", width: 180 }, - { key: "industries", width: 197 }, - { key: "profile", width: null }, -]; - export default function Matches() { const { t } = useI18n(); const [submittedQuery, setSubmittedQuery] = useState(""); @@ -131,80 +124,11 @@ export default function Matches() {
-
-
-
- {TABLE_HEADERS.map(({ key, width }) => ( -
- - {t(`matches_col_${key}`)} - -
- ))} -
-
- - {partners.map((partner, i) => ( - - ))} - - {!submittedQuery && ( -
- {t("matches_empty_prompt")} -
- )} - - {!isFetching && submittedQuery && partners.length === 0 && ( -
- {t("matches_no_results")} -
- )} +
From 1e00f4a0a36b36e2e351df3239311427a7207fa9 Mon Sep 17 00:00:00 2001 From: seongmin Date: Sun, 10 May 2026 01:04:12 +0900 Subject: [PATCH 06/19] =?UTF-8?q?[#11]Fix:=20PartnerSearchInput=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=EC=B0=BD=20=EB=86=92=EC=9D=B4=20=EC=A1=B0?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/PartnerSearchInput.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/PartnerSearchInput.jsx b/src/components/PartnerSearchInput.jsx index f5ff4aa..1f83839 100644 --- a/src/components/PartnerSearchInput.jsx +++ b/src/components/PartnerSearchInput.jsx @@ -62,6 +62,8 @@ export default function PartnerSearchInput({ onSearch, isLoading }) { fontSize: 16, color: "#080616", lineHeight: "24px", + padding: 0, + height: 24, }} />
From 586499408a4d2fe26baa317df9a4954f2e52cfaf Mon Sep 17 00:00:00 2001 From: seongmin Date: Sun, 10 May 2026 01:06:31 +0900 Subject: [PATCH 07/19] =?UTF-8?q?[#11]Fix:=20=ED=8C=8C=ED=8A=B8=EB=84=88?= =?UTF-8?q?=20=EA=B2=80=EC=83=89=20=EA=B2=B0=EA=B3=BC=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=20=EB=A7=A4=ED=95=91=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20?= =?UTF-8?q?=EC=97=85=EC=B2=B4=EB=AA=85=20=ED=95=98=EC=9D=B4=ED=8D=BC?= =?UTF-8?q?=EB=A7=81=ED=81=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/PartnerTable.jsx | 9 +++++---- src/components/PartnerTableRow.jsx | 8 ++++++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/components/PartnerTable.jsx b/src/components/PartnerTable.jsx index 071eaeb..2312f17 100644 --- a/src/components/PartnerTable.jsx +++ b/src/components/PartnerTable.jsx @@ -54,10 +54,11 @@ export default function PartnerTable({ partners, isFetching, submittedQuery }) { {partners.map((partner, i) => ( ))} diff --git a/src/components/PartnerTableRow.jsx b/src/components/PartnerTableRow.jsx index 8b1dd42..9c12215 100644 --- a/src/components/PartnerTableRow.jsx +++ b/src/components/PartnerTableRow.jsx @@ -4,6 +4,7 @@ export default function PartnerTableRow({ industries, profile, avatarUrl, + websiteUrl, }) { const industryText = Array.isArray(industries) ? industries.join(", ") @@ -64,7 +65,10 @@ export default function PartnerTableRow({ }} /> )} - {name} - +
Date: Sun, 10 May 2026 01:42:28 +0900 Subject: [PATCH 08/19] =?UTF-8?q?[#11]Feat:=20=EB=8B=A4=ED=81=AC=20?= =?UTF-8?q?=ED=91=B8=ED=84=B0=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=EC=A1=B0=EA=B1=B4=EB=B6=80=20?= =?UTF-8?q?=EB=A0=8C=EB=8D=94=EB=A7=81=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 12 ++ src/App.jsx | 3 +- src/assets/footer_logo_icon.svg | 3 + src/assets/footer_logo_text.svg | 6 + src/components/Footer.jsx | 336 ++++++++++++++++++++++++++------ 6 files changed, 302 insertions(+), 59 deletions(-) create mode 100644 src/assets/footer_logo_icon.svg create mode 100644 src/assets/footer_logo_text.svg diff --git a/package.json b/package.json index 0f5f796..bb31e50 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "react-dom": "^18.2.0", "react-hook-form": "^7.74.0", "react-hot-toast": "^2.6.0", + "react-icons": "^5.6.0", "react-router-dom": "^6.26.1", "recharts": "^3.5.0", "zustand": "^5.0.12" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index baa28ee..6e23fe3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: react-hot-toast: specifier: ^2.6.0 version: 2.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-icons: + specifier: ^5.6.0 + version: 5.6.0(react@18.3.1) react-router-dom: specifier: ^6.26.1 version: 6.30.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1633,6 +1636,11 @@ packages: react: '>=16' react-dom: '>=16' + react-icons@5.6.0: + resolution: {integrity: sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==} + peerDependencies: + react: '*' + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -3536,6 +3544,10 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + react-icons@5.6.0(react@18.3.1): + dependencies: + react: 18.3.1 + react-is@16.13.1: {} react-redux@9.2.0(react@18.3.1)(redux@5.0.1): diff --git a/src/App.jsx b/src/App.jsx index e01e5c6..5306977 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -31,7 +31,8 @@ export default function App() { >
-