From aeae1e3a8efed9e98a671bbd4d104ca2337a68f0 Mon Sep 17 00:00:00 2001 From: Benjamin Bolte Date: Wed, 31 Jul 2024 19:24:29 -0700 Subject: [PATCH 1/2] revamp frontend --- frontend/package-lock.json | 308 ++++++++++++++++++ frontend/package.json | 2 + .../components/auth/AuthenticationModal.tsx | 15 +- frontend/src/components/auth/LoginForm.tsx | 25 +- frontend/src/components/auth/SignupForm.tsx | 45 ++- .../components/listing/ListingDescription.tsx | 37 +++ .../components/listings/ListingGridCard.tsx | 19 +- frontend/src/components/nav/Sidebar.tsx | 3 + frontend/src/components/nav/TopNavbar.tsx | 9 +- .../components/ui/Breadcrumb/Breadcrumbs.tsx | 49 +++ frontend/src/components/ui/ErrorMessage.tsx | 2 +- frontend/src/components/ui/Header.tsx | 5 +- .../ui/Search/SearchInput.module.css | 30 -- .../src/components/ui/Search/SearchInput.tsx | 40 --- frontend/src/pages/Home.tsx | 50 +-- frontend/src/pages/ListingDetails.tsx | 17 +- frontend/src/pages/Listings.tsx | 111 ++++--- frontend/src/pages/NewListing.tsx | 214 +++++------- frontend/src/types/index.ts | 10 +- 19 files changed, 646 insertions(+), 345 deletions(-) create mode 100644 frontend/src/components/ui/Breadcrumb/Breadcrumbs.tsx delete mode 100644 frontend/src/components/ui/Search/SearchInput.module.css delete mode 100644 frontend/src/components/ui/Search/SearchInput.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8e72e222..2d829e24 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -17,6 +17,7 @@ "@types/node": "^16.18.97", "@types/react": "^18.3.2", "@types/react-dom": "^18.3.0", + "@uidotdev/usehooks": "^2.4.1", "browser-image-compression": "^2.0.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", @@ -34,6 +35,7 @@ "react-markdown": "^9.0.1", "react-scripts": "5.0.1", "react-spring": "^9.7.3", + "remark-gfm": "^4.0.0", "tailwind-merge": "^2.4.0", "urdf-loader": "^0.12.1", "uuid": "^10.0.0", @@ -7910,6 +7912,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@uidotdev/usehooks": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@uidotdev/usehooks/-/usehooks-2.4.1.tgz", + "integrity": "sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -19517,12 +19532,50 @@ "tmpl": "1.0.5" } }, + "node_modules/markdown-table": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz", + "integrity": "sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/marky": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/marky/-/marky-1.2.5.tgz", "integrity": "sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==", "peer": true }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz", + "integrity": "sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mdast-util-from-markdown": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.1.tgz", @@ -19547,6 +19600,107 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz", + "integrity": "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.0.tgz", + "integrity": "sha512-FyzMsduZZHSc3i0Px3PQcBT4WJY/X/RCtEJKuybiC6sjPqLv7h1yqAkmILZtuxMSsUyaLUWNp71+vQH2zqp5cg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz", + "integrity": "sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-mdx-expression": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.0.tgz", @@ -20371,6 +20525,127 @@ "micromark-util-types": "^2.0.0" } }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.0.tgz", + "integrity": "sha512-Ub2ncQv+fwD70/l4ou27b4YzfNaCJOvyX4HxXU15m7mpYY+rjuWzsLIPZHJL253Z643RpbcP1oeIJlQ/SKW67g==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/micromark-factory-destination": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz", @@ -26233,6 +26508,24 @@ "node": ">= 0.10" } }, + "node_modules/remark-gfm": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.0.tgz", + "integrity": "sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-parse": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", @@ -26266,6 +26559,21 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/renderkid": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 9aaeab25..e334c8c7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ "@types/node": "^16.18.97", "@types/react": "^18.3.2", "@types/react-dom": "^18.3.0", + "@uidotdev/usehooks": "^2.4.1", "browser-image-compression": "^2.0.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", @@ -29,6 +30,7 @@ "react-markdown": "^9.0.1", "react-scripts": "5.0.1", "react-spring": "^9.7.3", + "remark-gfm": "^4.0.0", "tailwind-merge": "^2.4.0", "urdf-loader": "^0.12.1", "uuid": "^10.0.0", diff --git a/frontend/src/components/auth/AuthenticationModal.tsx b/frontend/src/components/auth/AuthenticationModal.tsx index 07833927..a0346bf0 100644 --- a/frontend/src/components/auth/AuthenticationModal.tsx +++ b/frontend/src/components/auth/AuthenticationModal.tsx @@ -11,18 +11,17 @@ const LogInModal = () => { return ( - - Sign in to see this page + + + Log In + - - - Issues logging in? Send an email to - support@robolist.xyz - - ); }; diff --git a/frontend/src/components/auth/LoginForm.tsx b/frontend/src/components/auth/LoginForm.tsx index 0e03a7fb..ad141fac 100644 --- a/frontend/src/components/auth/LoginForm.tsx +++ b/frontend/src/components/auth/LoginForm.tsx @@ -3,7 +3,7 @@ import { Button } from "components/ui/Button/Button"; import ErrorMessage from "components/ui/ErrorMessage"; import { Input } from "components/ui/Input/Input"; import { useState } from "react"; -import { Eye } from "react-bootstrap-icons"; +import { Eye, EyeSlash } from "react-bootstrap-icons"; import { SubmitHandler, useForm } from "react-hook-form"; import { LoginSchema, LoginType } from "types"; @@ -31,8 +31,8 @@ const LoginForm = () => { {/* Email */}
- {errors?.email && {errors?.email?.message}}
+ {errors?.email && {errors?.email?.message}} {/* Password */}
@@ -41,16 +41,23 @@ const LoginForm = () => { type={showPassword ? "text" : "password"} {...register("password")} /> - {errors?.password && ( - {errors?.password?.message} - )}
- setShowPassword((p) => !p)} - className="cursor-pointer" - /> + {showPassword ? ( + setShowPassword(false)} + className="cursor-pointer" + /> + ) : ( + setShowPassword(true)} + className="cursor-pointer" + /> + )}
+ {errors?.password && ( + {errors?.password?.message} + )} diff --git a/frontend/src/components/listing/ListingDescription.tsx b/frontend/src/components/listing/ListingDescription.tsx index 68fe6a73..f534b04e 100644 --- a/frontend/src/components/listing/ListingDescription.tsx +++ b/frontend/src/components/listing/ListingDescription.tsx @@ -1,4 +1,41 @@ import { Row } from "react-bootstrap"; +import Markdown from "react-markdown"; +import remarkGfm from "remark-gfm"; + +interface RenderDescriptionProps { + description: string; +} + +export const RenderDescription = ({ description }: RenderDescriptionProps) => { + return ( +

{children}

, + ul: ({ children }) => , + ol: ({ children }) =>
    {children}
, + li: ({ children }) =>
  • {children}
  • , + table: ({ children }) => ( + {children}
    + ), + thead: ({ children }) => ( + {children} + ), + tbody: ({ children }) => {children}, + tr: ({ children }) => {children}, + th: ({ children }) => ( + {children} + ), + td: ({ children }) => ( + {children} + ), + }} + > + {description} +
    + ); +}; interface Props { description: string | null; diff --git a/frontend/src/components/listings/ListingGridCard.tsx b/frontend/src/components/listings/ListingGridCard.tsx index 2e9b6e6e..597cafcd 100644 --- a/frontend/src/components/listings/ListingGridCard.tsx +++ b/frontend/src/components/listings/ListingGridCard.tsx @@ -1,6 +1,6 @@ +import { RenderDescription } from "components/listing/ListingDescription"; import { paths } from "gen/api"; import { Card, Placeholder } from "react-bootstrap"; -import Markdown from "react-markdown"; import { useNavigate } from "react-router-dom"; type ListingInfo = @@ -22,20 +22,9 @@ const ListingGridCard = (props: Props) => { {part ? part.name : } -

    , - li: ({ ...props }) =>

  • , - h1: ({ ...props }) =>

    , - h2: ({ ...props }) =>

    , - h3: ({ ...props }) =>

    , - h4: ({ ...props }) =>
    , - h5: ({ ...props }) =>
    , - h6: ({ ...props }) =>
    , - }} - > - {part?.description} - + {part?.description && ( + + )} diff --git a/frontend/src/components/nav/Sidebar.tsx b/frontend/src/components/nav/Sidebar.tsx index 85c3c877..100350e0 100644 --- a/frontend/src/components/nav/Sidebar.tsx +++ b/frontend/src/components/nav/Sidebar.tsx @@ -21,6 +21,9 @@ const Sidebar = ({ show, onHide }: Props) => { }} > + + Log Out + About diff --git a/frontend/src/components/nav/TopNavbar.tsx b/frontend/src/components/nav/TopNavbar.tsx index 89823aa2..dee0e5aa 100644 --- a/frontend/src/components/nav/TopNavbar.tsx +++ b/frontend/src/components/nav/TopNavbar.tsx @@ -4,7 +4,7 @@ import { useAuthentication } from "hooks/auth"; import { useTheme } from "hooks/theme"; import { useState } from "react"; import { Container, Nav, Navbar } from "react-bootstrap"; -import { GearFill, MoonFill, SunFill } from "react-bootstrap-icons"; +import { MoonFill, PersonCircle, SunFill } from "react-bootstrap-icons"; import { Link } from "react-router-dom"; const TopNavbar = () => { @@ -31,17 +31,14 @@ const TopNavbar = () => { <> setShowSidebar(true)}> - + - - Log Out - ) : ( <> - Login + )} diff --git a/frontend/src/components/ui/Breadcrumb/Breadcrumbs.tsx b/frontend/src/components/ui/Breadcrumb/Breadcrumbs.tsx new file mode 100644 index 00000000..54680290 --- /dev/null +++ b/frontend/src/components/ui/Breadcrumb/Breadcrumbs.tsx @@ -0,0 +1,49 @@ +import clsx from "clsx"; + +interface ItemProps { + label: string; + onClick?: () => void; + logo?: React.ReactNode; +} + +interface Props { + items: ItemProps[]; +} + +const Breadcrumbs = ({ items }: Props) => { + return ( + + ); +}; + +export default Breadcrumbs; diff --git a/frontend/src/components/ui/ErrorMessage.tsx b/frontend/src/components/ui/ErrorMessage.tsx index 8a57d64d..48013c74 100644 --- a/frontend/src/components/ui/ErrorMessage.tsx +++ b/frontend/src/components/ui/ErrorMessage.tsx @@ -1,7 +1,7 @@ import React from "react"; const ErrorMessage = ({ children }: { children: React.ReactNode }) => { - return
    {children}
    ; + return
    {children}
    ; }; export default ErrorMessage; diff --git a/frontend/src/components/ui/Header.tsx b/frontend/src/components/ui/Header.tsx index bd15f8e0..ed07edee 100644 --- a/frontend/src/components/ui/Header.tsx +++ b/frontend/src/components/ui/Header.tsx @@ -1,13 +1,14 @@ import { cn } from "utils"; interface HeaderProps { + title?: string; label?: string; } -const Header = ({ label }: HeaderProps) => { +const Header = ({ title, label }: HeaderProps) => { return (
    -

    Robolist

    +

    {title ?? "Robolist"}

    {label &&

    {label}

    }
    ); diff --git a/frontend/src/components/ui/Search/SearchInput.module.css b/frontend/src/components/ui/Search/SearchInput.module.css deleted file mode 100644 index 219ea007..00000000 --- a/frontend/src/components/ui/Search/SearchInput.module.css +++ /dev/null @@ -1,30 +0,0 @@ -.SearchInput { - display: flex; - align-items: center; - border: 1px solid gray; - border-radius: 5px; - width: 50%; -} - -.SearchInput:focus-within { - box-shadow: 0 0 0 2px blue; -} - -.Icon { - margin-left: 0.3em; - filter: opacity(50%); -} - -.SearchInput:focus-within .Icon { - filter: opacity(100%); -} - -.SearchInput .Input { - border: none; - overflow: hidden; - background-color: transparent; -} - -.SearchInput .Input:focus { - box-shadow: none; -} diff --git a/frontend/src/components/ui/Search/SearchInput.tsx b/frontend/src/components/ui/Search/SearchInput.tsx deleted file mode 100644 index 2f9fcffc..00000000 --- a/frontend/src/components/ui/Search/SearchInput.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import * as React from "react"; -import { Search } from "react-bootstrap-icons"; -import { Input } from "../Input/Input"; -import styles from "./SearchInput.module.css"; - -export interface SearchInputProps - extends React.InputHTMLAttributes { - userInput?: string; - onSearch?: (query: string) => void; -} - -const SearchInput = ({ - className, - userInput, - onChange, - onSearch, -}: SearchInputProps) => { - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter" && onSearch && userInput !== undefined) { - onSearch(userInput); // Trigger a new callback onSearch - } - }; - return ( -
    - - -
    - ); -}; - -SearchInput.displayName = "SearchInput"; - -export { SearchInput }; diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 427dee76..2bd60fa4 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -1,4 +1,6 @@ -import { Card, Col, Row } from "react-bootstrap"; +import { Card, CardContent, CardHeader, CardTitle } from "components/ui/Card"; +import Header from "components/ui/Header"; +import { Row } from "react-bootstrap"; import { useNavigate } from "react-router-dom"; const Home = () => { @@ -11,30 +13,28 @@ const Home = () => {

    Buy and sell robots and robot parts

    - - navigate(`/listings`)} - className="text-center" - bg="secondary" - > - - Browse Listings - Browse existing Robolist listings - - - - - navigate(`/listings/add`)} - className="text-center" - bg="primary" - > - - Create Listing - List your robot on Robolist - - - + navigate(`/listings`)} + className="w-[400px] shadow-md h-full mb-40" + > + +
    + + + Browse existing Robolist listings + + + navigate(`/listings/add`)} + className="w-[400px] shadow-md h-full mb-40 ml-4" + > + +
    + + + List your robot on Robolist + + ); diff --git a/frontend/src/pages/ListingDetails.tsx b/frontend/src/pages/ListingDetails.tsx index 2804dbd4..5e71c126 100644 --- a/frontend/src/pages/ListingDetails.tsx +++ b/frontend/src/pages/ListingDetails.tsx @@ -3,11 +3,12 @@ import ListingChildren from "components/listing/ListingChildren"; import ListingDeleteButton from "components/listing/ListingDeleteButton"; import ListingDescription from "components/listing/ListingDescription"; import ListingTitle from "components/listing/ListingTitle"; +import Breadcrumbs from "components/ui/Breadcrumb/Breadcrumbs"; import { paths } from "gen/api"; import { useAlertQueue } from "hooks/alerts"; import { useAuthentication } from "hooks/auth"; import { useEffect, useState } from "react"; -import { Breadcrumb, Col, Container, Row, Spinner } from "react-bootstrap"; +import { Col, Container, Row, Spinner } from "react-bootstrap"; import { useNavigate, useParams } from "react-router-dom"; type ListingResponse = @@ -70,13 +71,13 @@ const ListingDetails = () => { return ( <> - - navigate("/")}>Home - navigate("/listings")}> - Listings - - {listing && {listing.name}} - + navigate("/") }, + { label: "Listings", onClick: () => navigate("/listings") }, + { label: listing?.name || "", onClick: undefined }, + ]} + /> {listing && id ? ( diff --git a/frontend/src/pages/Listings.tsx b/frontend/src/pages/Listings.tsx index e9970028..49aca967 100644 --- a/frontend/src/pages/Listings.tsx +++ b/frontend/src/pages/Listings.tsx @@ -1,9 +1,10 @@ +import { useDebounce } from "@uidotdev/usehooks"; import ListingGrid from "components/listings/ListingGrid"; -import { SearchInput } from "components/ui/Search/SearchInput"; +import Breadcrumbs from "components/ui/Breadcrumb/Breadcrumbs"; +import { Input } from "components/ui/Input/Input"; import { useAlertQueue } from "hooks/alerts"; import { useAuthentication } from "hooks/auth"; import { useEffect, useState } from "react"; -import { Breadcrumb } from "react-bootstrap"; import { useNavigate, useParams } from "react-router-dom"; const Listings = () => { @@ -11,11 +12,15 @@ const Listings = () => { const [listingIds, setListingIds] = useState(null); const [moreListings, setMoreListings] = useState(false); const [searchQuery, setSearchQuery] = useState(""); - const [visibleSearchBarInput, setVisibleSearchBarInput] = useState(""); const { addErrorAlert } = useAlertQueue(); const navigate = useNavigate(); + const debouncedSearch = useDebounce(searchQuery, 300); + useEffect(() => { + handleSearch(); + }, [debouncedSearch]); + // Gets the current page number and makes sure it is valid. const { page } = useParams(); const pageNumber = parseInt(page || "1", 10); @@ -23,64 +28,68 @@ const Listings = () => { navigate("/404"); } - function handleSearch() { - setSearchQuery(visibleSearchBarInput); - } - - const handleSearchInputEnterKey = (query: string) => { - setVisibleSearchBarInput(query); - handleSearch(); - }; + const handleSearch = async () => { + setListingIds(null); - useEffect(() => { - (async () => { - const { data, error } = await auth.client.GET("/listings/search", { - params: { - query: { - page: pageNumber, - search_query: searchQuery, - }, + const { data, error } = await auth.client.GET("/listings/search", { + params: { + query: { + page: pageNumber, + search_query: searchQuery, }, - }); - - if (error) { - addErrorAlert(error); - return; - } + }, + }); + if (error) { + addErrorAlert(error); + } else { setListingIds(data.listing_ids); setMoreListings(data.has_next); - })(); - }, [pageNumber, searchQuery]); + } + }; + + const prevButton = pageNumber > 1; + const nextButton = moreListings; + const hasButton = prevButton || nextButton; return ( <> - - navigate("/")}>Home - Listings - - setVisibleSearchBarInput(e.target.value)} - onSearch={handleSearchInputEnterKey} + navigate("/") }, + { label: "Listings" }, + ]} /> - - {pageNumber > 1 && ( - - )} - {moreListings && ( - +
    + setSearchQuery(e.target.value)} + placeholder="Search listings..." + className="w-[500px]" + /> +
    + {hasButton && ( +
    +
    + {prevButton && ( + + )} + {nextButton && ( + + )} +
    +
    )} + ); }; diff --git a/frontend/src/pages/NewListing.tsx b/frontend/src/pages/NewListing.tsx index b68bc33d..ec4bfcc4 100644 --- a/frontend/src/pages/NewListing.tsx +++ b/frontend/src/pages/NewListing.tsx @@ -1,163 +1,109 @@ +import { zodResolver } from "@hookform/resolvers/zod"; import RequireAuthentication from "components/auth/RequireAuthentication"; import TCButton from "components/files/TCButton"; -import { paths } from "gen/api"; +import { RenderDescription } from "components/listing/ListingDescription"; +import { Card, CardContent, CardHeader } from "components/ui/Card"; +import ErrorMessage from "components/ui/ErrorMessage"; +import Header from "components/ui/Header"; +import { Input } from "components/ui/Input/Input"; import { useAlertQueue } from "hooks/alerts"; import { useAuthentication } from "hooks/auth"; -import { useTheme } from "hooks/theme"; -import { ChangeEvent, FormEvent, useEffect, useState } from "react"; -import { Col, Form, Row } from "react-bootstrap"; +import { useState } from "react"; +import { Col } from "react-bootstrap"; +import { useForm } from "react-hook-form"; import { useNavigate } from "react-router-dom"; - -type ListingDumpResponse = - paths["/listings/dump"]["get"]["responses"][200]["content"]["application/json"]; +import { NewListingSchema, NewListingType } from "types"; const NewListing = () => { const auth = useAuthentication(); - const [name, setName] = useState(""); - const [description, setDescription] = useState(""); - const [listings, setListings] = useState(null); - const [children, setChildren] = useState([]); // Store the ids of each child - const { addAlert, addErrorAlert } = useAlertQueue(); - const { theme } = useTheme(); const navigate = useNavigate(); - const handleChildrenChange = ( - index: number, - e: ChangeEvent, - ) => { - const { value } = e.target; - const newChildren = [...children]; - newChildren[index] = value; - setChildren(newChildren); - }; - - const handleAddChild = () => { - setChildren([...children, ""]); - }; - - const handleRemoveChild = (index: number) => { - const newChildren = children.filter((_, i) => i !== index); - setChildren(newChildren); - }; + const [description, setDescription] = useState(""); - // Fetch all listings to use for children. - useEffect(() => { - const fetchListings = async () => { - try { - const { data, error } = await auth.client.GET("/listings/dump"); - if (error) { - addErrorAlert(error); - } else { - setListings(data); - } - } catch (err) { - addErrorAlert(err); - } - }; - fetchListings(); - }, []); + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(NewListingSchema), + }); // On submit, add the listing to the database and navigate to the // newly-created listing. - const handleSubmit = async (event: FormEvent) => { - event.preventDefault(); - - const { data, error } = await auth.client.POST("/listings/add", { - body: { - name, - description, - child_ids: [], + const onSubmit = async ({ name, description }: NewListingType) => { + const { data: responseData, error } = await auth.client.POST( + "/listings/add", + { + body: { + name, + description, + child_ids: [], + }, }, - }); + ); if (error) { addErrorAlert(error); } else { addAlert("Listing added successfully", "success"); - navigate(`/listing/${data.listing_id}`); + navigate(`/listing/${responseData.listing_id}`); } }; return ( -

    Create Listing

    -
    - {/* Name */} - - { - setName(e.target.value); - }} - value={name} - required - /> - - {/* Description */} - - { - setDescription(e.target.value); - }} - value={description} - /> - -

    Children

    - {children.map((id, index) => ( - - - - handleChildrenChange(index, e)} - required - > - - {listings && - listings.listings.map((listing, index) => ( - - ))} - - - - handleRemoveChild(index)} - > - Remove - - - - ))} - - - Add Child - - - {/* Submit */} - - Submit - - +
    + + +
    + + +
    + {/* Name */} +
    + + {errors?.name && ( + {errors?.name?.message} + )} +
    + + {/* Description Input */} +
    +