From 1969b6ef5ef15fdfad2c0fb4a46ca88a4bd4d621 Mon Sep 17 00:00:00 2001 From: Benjamin Bolte Date: Wed, 31 Jul 2024 16:36:09 -0700 Subject: [PATCH 1/3] fix up login flow --- frontend/src/App.tsx | 6 +- frontend/src/components/auth/AuthBlock.tsx | 95 +++++++++++++ .../components/{ui => auth}/AuthProvider.tsx | 2 +- .../components/auth/AuthenticationModal.tsx | 6 +- frontend/src/components/auth/LoginForm.tsx | 62 +++++++++ frontend/src/components/auth/SignupForm.tsx | 81 +++++++++++ .../src/components/listings/ListingGrid.tsx | 63 +++++++++ .../components/listings/ListingGridCard.tsx | 45 ++++++ .../src/components/ui/Button/BackButton.tsx | 9 +- .../src/components/ui/{Card => }/Card.tsx | 0 .../src/components/ui/Card/CardWrapper.tsx | 45 ------ frontend/src/components/ui/Header.tsx | 4 +- frontend/src/gen/api.ts | 28 +++- frontend/src/pages/Home.tsx | 61 +++----- frontend/src/pages/Listings.tsx | 130 ++---------------- frontend/src/pages/Login.tsx | 11 ++ frontend/src/pages/MyListings.tsx | 127 ----------------- frontend/src/pages/NewListing.tsx | 5 +- frontend/src/pages/auth/Login.tsx | 120 ---------------- frontend/src/pages/auth/Signup.tsx | 88 ------------ store/app/routers/listings.py | 39 +++++- store/app/routers/users.py | 3 +- 22 files changed, 461 insertions(+), 569 deletions(-) create mode 100644 frontend/src/components/auth/AuthBlock.tsx rename frontend/src/components/{ui => auth}/AuthProvider.tsx (96%) create mode 100644 frontend/src/components/auth/LoginForm.tsx create mode 100644 frontend/src/components/auth/SignupForm.tsx create mode 100644 frontend/src/components/listings/ListingGrid.tsx create mode 100644 frontend/src/components/listings/ListingGridCard.tsx rename frontend/src/components/ui/{Card => }/Card.tsx (100%) delete mode 100644 frontend/src/components/ui/Card/CardWrapper.tsx create mode 100644 frontend/src/pages/Login.tsx delete mode 100644 frontend/src/pages/MyListings.tsx delete mode 100644 frontend/src/pages/auth/Login.tsx delete mode 100644 frontend/src/pages/auth/Signup.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 09774b15..79256732 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,13 +6,11 @@ import { AlertQueue, AlertQueueProvider } from "hooks/alerts"; import { AuthenticationProvider } from "hooks/auth"; import { ThemeProvider } from "hooks/theme"; import About from "pages/About"; -import Login from "pages/auth/Login"; -import Signup from "pages/auth/Signup"; import Home from "pages/Home"; import ListingDetails from "pages/ListingDetails"; import Listings from "pages/Listings"; +import Login from "pages/Login"; import Logout from "pages/Logout"; -import MyListings from "pages/MyListings"; import NewListing from "pages/NewListing"; import NotFound from "pages/NotFound"; import { Container } from "react-bootstrap"; @@ -33,12 +31,10 @@ const App = () => { } /> } /> } /> - } /> } /> } /> } /> } /> - } /> } /> } /> diff --git a/frontend/src/components/auth/AuthBlock.tsx b/frontend/src/components/auth/AuthBlock.tsx new file mode 100644 index 00000000..24e4e569 --- /dev/null +++ b/frontend/src/components/auth/AuthBlock.tsx @@ -0,0 +1,95 @@ +import BackButton from "components/ui/Button/BackButton"; +import { Card, CardContent, CardFooter, CardHeader } from "components/ui/Card"; +import Header from "components/ui/Header"; +import { useAlertQueue } from "hooks/alerts"; +import { useAuthentication } from "hooks/auth"; +import { useEffect, useState } from "react"; +import { Spinner } from "react-bootstrap"; +import AuthProvider from "./AuthProvider"; +import LoginForm from "./LoginForm"; +import SignupForm from "./SignupForm"; + +export const AuthBlockInner = () => { + const auth = useAuthentication(); + const { addErrorAlert } = useAlertQueue(); + + const [isSignup, setIsSignup] = useState(false); + const [useSpinner, setUseSpinner] = useState(false); + + const handleGithubSubmit = async ( + event: React.MouseEvent, + ) => { + event.preventDefault(); + + const { data, error } = await auth.client.GET("/users/github/login"); + if (error) { + addErrorAlert(error); + } else { + window.open(data, "_self"); + } + }; + + useEffect(() => { + (async () => { + // Get the code from the query string to carry out OAuth login. + const search = window.location.search; + const params = new URLSearchParams(search); + const code = params.get("code"); + + if (code) { + setUseSpinner(true); + const { data, error } = await auth.client.POST("/users/github/code", { + body: { code }, + }); + + if (error) { + addErrorAlert(error); + setUseSpinner(false); + } else { + auth.login(data.api_key); + setUseSpinner(false); + } + } + })(); + }, []); + + if (useSpinner) { + return ( + + + + ); + } + + return ( + <> + {isSignup ? : } + + + + + setIsSignup((s) => !s)} + label={ + isSignup + ? "Already have an account? Login here." + : "Don't have an account? Create a new account." + } + /> + + + ); +}; + +const AuthBlock = () => { + return ( + + +
+ + + + ); +}; + +export default AuthBlock; diff --git a/frontend/src/components/ui/AuthProvider.tsx b/frontend/src/components/auth/AuthProvider.tsx similarity index 96% rename from frontend/src/components/ui/AuthProvider.tsx rename to frontend/src/components/auth/AuthProvider.tsx index 98e4e7b5..da386036 100644 --- a/frontend/src/components/ui/AuthProvider.tsx +++ b/frontend/src/components/auth/AuthProvider.tsx @@ -1,6 +1,6 @@ import { FaGithub } from "react-icons/fa"; import { FcGoogle } from "react-icons/fc"; -import { Button } from "./Button/Button"; +import { Button } from "../ui/Button/Button"; interface AuthProvider { handleGoogleSubmit?: () => void; diff --git a/frontend/src/components/auth/AuthenticationModal.tsx b/frontend/src/components/auth/AuthenticationModal.tsx index a38eda11..07833927 100644 --- a/frontend/src/components/auth/AuthenticationModal.tsx +++ b/frontend/src/components/auth/AuthenticationModal.tsx @@ -1,5 +1,6 @@ import { Modal } from "react-bootstrap"; import { useNavigate } from "react-router-dom"; +import { AuthBlockInner } from "./AuthBlock"; const LogInModal = () => { const navigate = useNavigate(); @@ -10,8 +11,11 @@ const LogInModal = () => { return ( + + Sign in to see this page + -

Sign in to see this page

+
diff --git a/frontend/src/components/auth/LoginForm.tsx b/frontend/src/components/auth/LoginForm.tsx new file mode 100644 index 00000000..0e03a7fb --- /dev/null +++ b/frontend/src/components/auth/LoginForm.tsx @@ -0,0 +1,62 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +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 { SubmitHandler, useForm } from "react-hook-form"; +import { LoginSchema, LoginType } from "types"; + +const LoginForm = () => { + const [showPassword, setShowPassword] = useState(false); + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(LoginSchema), + }); + + const onSubmit: SubmitHandler = async (data: LoginType) => { + // TODO: Add an api endpoint to send the credentials details to backend and email verification. + console.log(data); + }; + + return ( +
+ {/* Email */} +
+ + {errors?.email && {errors?.email?.message}} +
+ + {/* Password */} +
+ + {errors?.password && ( + {errors?.password?.message} + )} +
+ setShowPassword((p) => !p)} + className="cursor-pointer" + /> +
+
+ + +
+ ); +}; + +export default LoginForm; diff --git a/frontend/src/components/auth/SignupForm.tsx b/frontend/src/components/auth/SignupForm.tsx new file mode 100644 index 00000000..a2b63b3e --- /dev/null +++ b/frontend/src/components/auth/SignupForm.tsx @@ -0,0 +1,81 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +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 { SubmitHandler, useForm } from "react-hook-form"; +import { SignUpSchema, SignupType } from "types"; + +const SignupForm = () => { + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = + useState(false); + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(SignUpSchema), + }); + + const onSubmit: SubmitHandler = async (data: SignupType) => { + // TODO: Add an api endpoint to send the credentials details to backend and email verification. + console.log(data); + }; + + return ( +
+ {/* Email */} +
+ + {errors?.email && {errors?.email?.message}} +
+ + {/* Password */} +
+ + {errors?.password && ( + {errors?.password?.message} + )} +
+ setShowPassword((p) => !p)} + className="cursor-pointer" + /> +
+
+ + {/* Confirm Password */} +
+ + {errors?.confirmPassword && ( + {errors?.confirmPassword?.message} + )} +
+ setShowConfirmPassword((p) => !p)} + className="cursor-pointer" + /> +
+
+ +
+ ); +}; + +export default SignupForm; diff --git a/frontend/src/components/listings/ListingGrid.tsx b/frontend/src/components/listings/ListingGrid.tsx new file mode 100644 index 00000000..132bf883 --- /dev/null +++ b/frontend/src/components/listings/ListingGrid.tsx @@ -0,0 +1,63 @@ +import { paths } from "gen/api"; +import { useAlertQueue } from "hooks/alerts"; +import { useAuthentication } from "hooks/auth"; +import { useEffect, useState } from "react"; +import { Col, Row, Spinner } from "react-bootstrap"; +import ListingGridCard from "./ListingGridCard"; + +type ListingInfo = + paths["/listings/batch"]["get"]["responses"][200]["content"]["application/json"]["listings"]; + +interface Props { + listingIds: string[] | null; +} + +const ListingGrid = (props: Props) => { + const { listingIds } = props; + const auth = useAuthentication(); + const { addErrorAlert } = useAlertQueue(); + + const [listingInfo, setListingInfoResponse] = useState( + null, + ); + + useEffect(() => { + if (listingIds !== null && listingIds.length > 0) { + (async () => { + console.log("LISTING IDS:", listingIds); + const { data, error } = await auth.client.GET("/listings/batch", { + params: { + query: { + ids: listingIds, + }, + }, + }); + + if (error) { + addErrorAlert(error); + return; + } + + setListingInfoResponse(data.listings); + })(); + } + }, [listingIds]); + + return ( + + {listingIds === null ? ( + + + + ) : ( + listingIds.map((listingId) => ( + + + + )) + )} + + ); +}; + +export default ListingGrid; diff --git a/frontend/src/components/listings/ListingGridCard.tsx b/frontend/src/components/listings/ListingGridCard.tsx new file mode 100644 index 00000000..2e9b6e6e --- /dev/null +++ b/frontend/src/components/listings/ListingGridCard.tsx @@ -0,0 +1,45 @@ +import { paths } from "gen/api"; +import { Card, Placeholder } from "react-bootstrap"; +import Markdown from "react-markdown"; +import { useNavigate } from "react-router-dom"; + +type ListingInfo = + paths["/listings/batch"]["get"]["responses"][200]["content"]["application/json"]["listings"]; + +interface Props { + listingId: string; + listingInfo: ListingInfo | null; +} + +const ListingGridCard = (props: Props) => { + const { listingId, listingInfo } = props; + const navigate = useNavigate(); + + const part = listingInfo?.find((listing) => listing.id === listingId); + + return ( + navigate(`/listing/${listingId}`)}> + + {part ? part.name : } + +

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

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

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

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

    , + h4: ({ ...props }) =>
    , + h5: ({ ...props }) =>
    , + h6: ({ ...props }) =>
    , + }} + > + {part?.description} + + + + + ); +}; + +export default ListingGridCard; diff --git a/frontend/src/components/ui/Button/BackButton.tsx b/frontend/src/components/ui/Button/BackButton.tsx index f26a6ca8..9809b55f 100644 --- a/frontend/src/components/ui/Button/BackButton.tsx +++ b/frontend/src/components/ui/Button/BackButton.tsx @@ -1,15 +1,14 @@ -import { Link } from "react-router-dom"; import { Button } from "./Button"; interface BackButtonProps { - href: string; label: string; + onClick: () => void; } -const BackButton = ({ href, label }: BackButtonProps) => { +const BackButton = ({ label, onClick }: BackButtonProps) => { return ( - ); }; diff --git a/frontend/src/components/ui/Card/Card.tsx b/frontend/src/components/ui/Card.tsx similarity index 100% rename from frontend/src/components/ui/Card/Card.tsx rename to frontend/src/components/ui/Card.tsx diff --git a/frontend/src/components/ui/Card/CardWrapper.tsx b/frontend/src/components/ui/Card/CardWrapper.tsx deleted file mode 100644 index 9c169884..00000000 --- a/frontend/src/components/ui/Card/CardWrapper.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import BackButton from "../Button/BackButton"; -import { Card, CardContent, CardFooter, CardHeader } from "../Card/Card"; -// import Header from -import AuthProvider from "../AuthProvider"; -import Header from "../Header"; - -interface CardWrapperProps { - children: React.ReactNode; - headerLabel: string; - backButtonLabel: string; - backButtonHref: string; - showProvider?: boolean; - loginWithGoogle?: () => void; - loginWithGithub?: ( - event: React.MouseEvent, - ) => Promise; -} - -const CardWrapper = ({ - children, - headerLabel, - backButtonLabel, - backButtonHref, - showProvider, - loginWithGithub, -}: CardWrapperProps) => { - return ( - - -
    - - {children} - {showProvider && ( - - - - )} - - - - - ); -}; - -export default CardWrapper; diff --git a/frontend/src/components/ui/Header.tsx b/frontend/src/components/ui/Header.tsx index 187ffb0a..64484759 100644 --- a/frontend/src/components/ui/Header.tsx +++ b/frontend/src/components/ui/Header.tsx @@ -1,14 +1,14 @@ import { cn } from "utils"; interface HeaderProps { - label: string; + label?: string; } const Header = ({ label }: HeaderProps) => { return (

    RoboList

    -

    {label}

    + {label &&

    {label}

    }
    ); }; diff --git a/frontend/src/gen/api.ts b/frontend/src/gen/api.ts index 23491518..1be49ac1 100644 --- a/frontend/src/gen/api.ts +++ b/frontend/src/gen/api.ts @@ -165,8 +165,8 @@ export interface paths { path?: never; cookie?: never; }; - /** Get Batch */ - get: operations["get_batch_listings_batch_get"]; + /** Get Batch Listing Info */ + get: operations["get_batch_listing_info_listings_batch_get"]; put?: never; post?: never; delete?: never; @@ -381,6 +381,11 @@ export interface components { /** Listings */ listings: components["schemas"]["Listing"][]; }; + /** GetBatchListingsResponse */ + GetBatchListingsResponse: { + /** Listings */ + listings: components["schemas"]["ListingInfoResponse"][]; + }; /** GetListingResponse */ GetListingResponse: { /** Id */ @@ -434,8 +439,8 @@ export interface components { }; /** ListListingsResponse */ ListListingsResponse: { - /** Listings */ - listings: components["schemas"]["Listing"][]; + /** Listing Ids */ + listing_ids: string[]; /** * Has Next * @default false @@ -461,6 +466,17 @@ export interface components { /** Description */ description: string | null; }; + /** ListingInfoResponse */ + ListingInfoResponse: { + /** Id */ + id: string; + /** Name */ + name: string; + /** Description */ + description: string | null; + /** Child Ids */ + child_ids: string[]; + }; /** NewListingRequest */ NewListingRequest: { /** Name */ @@ -764,7 +780,7 @@ export interface operations { }; }; }; - get_batch_listings_batch_get: { + get_batch_listing_info_listings_batch_get: { parameters: { query: { /** @description List of part ids */ @@ -782,7 +798,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ListListingsResponse"]; + "application/json": components["schemas"]["GetBatchListingsResponse"]; }; }; /** @description Validation Error */ diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index b6086366..427dee76 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -1,11 +1,8 @@ -import TCButton from "components/files/TCButton"; -import { useAuthentication } from "hooks/auth"; import { Card, Col, Row } from "react-bootstrap"; import { useNavigate } from "react-router-dom"; const Home = () => { const navigate = useNavigate(); - const { isAuthenticated } = useAuthentication(); return (
    @@ -14,51 +11,31 @@ const Home = () => {

    Buy and sell robots and robot parts

    - - navigate(`/listings`)}> + + navigate(`/listings`)} + className="text-center" + bg="secondary" + > Browse Listings - Buy and sell robots or robot parts + Browse existing Robolist listings + + + + + navigate(`/listings/add`)} + className="text-center" + bg="primary" + > + + Create Listing + List your robot on Robolist - - {isAuthenticated && ( - <> - - - { - navigate("/listings/me/1"); - }} - > - View My Listings - - - - - { - navigate("/listings/add"); - }} - > - Make a Listing - - - - - )}
    ); }; diff --git a/frontend/src/pages/Listings.tsx b/frontend/src/pages/Listings.tsx index b4c347fb..9f576f2a 100644 --- a/frontend/src/pages/Listings.tsx +++ b/frontend/src/pages/Listings.tsx @@ -1,46 +1,30 @@ +import ListingGrid from "components/listings/ListingGrid"; import { SearchInput } from "components/ui/Search/SearchInput"; -import { paths } from "gen/api"; import { useAlertQueue } from "hooks/alerts"; import { useAuthentication } from "hooks/auth"; import { useEffect, useState } from "react"; -import { - Breadcrumb, - Card, - Col, - Container, - Row, - Spinner, -} from "react-bootstrap"; -import Markdown from "react-markdown"; -import { Link, useNavigate, useParams } from "react-router-dom"; - -type ListingsType = - paths["/listings/search"]["get"]["responses"][200]["content"]["application/json"]["listings"]; +import { Breadcrumb } from "react-bootstrap"; +import { useNavigate, useParams } from "react-router-dom"; const Listings = () => { const auth = useAuthentication(); - const [partsData, setListings] = useState(null); + const [listingIds, setListingIds] = useState(null); const [moreListings, setMoreListings] = useState(false); - const [idMap, setIdMap] = useState>(new Map()); const [searchQuery, setSearchQuery] = useState(""); const [visibleSearchBarInput, setVisibleSearchBarInput] = useState(""); const { addErrorAlert } = useAlertQueue(); - const { page } = useParams(); - const pageNumber = parseInt(page || "1", 10); + const navigate = useNavigate(); + // Gets the current page number and makes sure it is valid. + const { page } = useParams(); + const pageNumber = parseInt(page || "1", 10); if (isNaN(pageNumber) || pageNumber < 0) { - return ( - <> -

    Listings

    -

    Invalid page number in URL.

    - - ); + navigate("/404"); } function handleSearch() { - const searchQuery = visibleSearchBarInput; - setSearchQuery(searchQuery); + setSearchQuery(visibleSearchBarInput); } const handleSearchInputEnterKey = (query: string) => { @@ -49,7 +33,7 @@ const Listings = () => { }; useEffect(() => { - const fetch_robots = async () => { + (async () => { const { data, error } = await auth.client.GET("/listings/search", { params: { query: { @@ -64,52 +48,10 @@ const Listings = () => { return; } - setListings(data.listings); + setListingIds(data.listing_ids); setMoreListings(data.has_next); - const ids = new Set(); - data.listings.forEach((part) => { - ids.add(part.user_id); - }); - - if (ids.size > 0) { - const { data, error } = await auth.client.GET("/users/batch", { - params: { - query: { - ids: Array.from(ids), - }, - }, - }); - - if (error) { - addErrorAlert(error); - return; - } - - const idMap = new Map(); - data.users.forEach((user) => { - idMap.set(user.id, user.email); - }); - setIdMap(idMap); - } - }; - fetch_robots(); + })(); }, [pageNumber, searchQuery]); - const navigate = useNavigate(); - - if (!partsData) { - return ( - - - - - - - - ); - } return ( <> @@ -122,51 +64,7 @@ const Listings = () => { onChange={(e) => setVisibleSearchBarInput(e.target.value)} onSearch={handleSearchInputEnterKey} /> - - - {partsData.map((part) => ( - - navigate(`/listing/${part.id}`)}> - - {part.name} - - {idMap.get(part.user_id) || "Unknown"} - - -

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

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

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

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

    , - h4: ({ ...props }) =>
    , - h5: ({ ...props }) =>
    , - h6: ({ ...props }) =>
    , - }} - > - {part.description} - - - - - - ))} - - {(pageNumber > 1 || moreListings) && ( - - {pageNumber > 1 && ( - - Previous Page - - )} - {moreListings && ( - - Next Page - - )} - - )} + ); }; diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx new file mode 100644 index 00000000..87cae7f7 --- /dev/null +++ b/frontend/src/pages/Login.tsx @@ -0,0 +1,11 @@ +import AuthBlock from "components/auth/AuthBlock"; + +const Auth = () => { + return ( +
    + +
    + ); +}; + +export default Auth; diff --git a/frontend/src/pages/MyListings.tsx b/frontend/src/pages/MyListings.tsx deleted file mode 100644 index 5e77940d..00000000 --- a/frontend/src/pages/MyListings.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { paths } from "gen/api"; -import { useAlertQueue } from "hooks/alerts"; -import { useAuthentication } from "hooks/auth"; -import { useEffect, useState } from "react"; -import { - Breadcrumb, - Card, - Col, - Container, - Row, - Spinner, -} from "react-bootstrap"; -import Markdown from "react-markdown"; -import { Link, useNavigate, useParams } from "react-router-dom"; - -type ListingsType = - paths["/listings/me"]["get"]["responses"][200]["content"]["application/json"]["listings"]; - -const MyListings = () => { - const auth = useAuthentication(); - const [partsData, setListings] = useState(null); - const { addErrorAlert } = useAlertQueue(); - const { page } = useParams(); - const [moreListings, setMoreListings] = useState(false); - - const pageNumber = parseInt(page || "1", 10); - - if (isNaN(pageNumber) || pageNumber < 0) { - return ( - <> -

    Robots

    -

    Invalid page number in URL.

    - - ); - } - - useEffect(() => { - const fetch_parts = async () => { - // const partsQuery = await auth_api.getMyListings(pageNumber); - const { data, error } = await auth.client.GET("/listings/me", { - params: { - query: { - page: pageNumber, - }, - }, - }); - - if (error) { - addErrorAlert(error); - } else { - setListings(data.listings); - setMoreListings(data.has_next); - } - }; - fetch_parts(); - }, []); - - const navigate = useNavigate(); - - if (!partsData) { - return ( - - - - - - - - ); - } - - return ( - <> - - navigate("/")}>Home - My Listings - - - - {partsData.map((part) => ( - - navigate(`/listing/${part.id}`)}> - - {part.name} - -

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

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

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

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

    , - h4: ({ ...props }) =>
    , - h5: ({ ...props }) =>
    , - h6: ({ ...props }) =>
    , - }} - > - {part.description} - - - - - - ))} - - {(pageNumber > 1 || moreListings) && ( - - {pageNumber > 1 && ( - - Previous Page - - )} - {moreListings && ( - - Next Page - - )} - - )} - - ); -}; - -export default MyListings; diff --git a/frontend/src/pages/NewListing.tsx b/frontend/src/pages/NewListing.tsx index af8f550c..b68bc33d 100644 --- a/frontend/src/pages/NewListing.tsx +++ b/frontend/src/pages/NewListing.tsx @@ -1,3 +1,4 @@ +import RequireAuthentication from "components/auth/RequireAuthentication"; import TCButton from "components/files/TCButton"; import { paths } from "gen/api"; import { useAlertQueue } from "hooks/alerts"; @@ -79,7 +80,7 @@ const NewListing = () => { }; return ( - <> +

    Create Listing

    {/* Name */} @@ -157,7 +158,7 @@ const NewListing = () => { Submit
    - +
    ); }; diff --git a/frontend/src/pages/auth/Login.tsx b/frontend/src/pages/auth/Login.tsx deleted file mode 100644 index d72c05e2..00000000 --- a/frontend/src/pages/auth/Login.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { zodResolver } from "@hookform/resolvers/zod"; -import { Button } from "components/ui/Button/Button"; -import CardWrapper from "components/ui/Card/CardWrapper"; -import ErrorMessage from "components/ui/ErrorMessage"; -import { Input } from "components/ui/Input/Input"; -import { useAlertQueue } from "hooks/alerts"; -import { useAuthentication } from "hooks/auth"; -import { useEffect, useState } from "react"; -import { Spinner } from "react-bootstrap"; -import { Eye } from "react-bootstrap-icons"; -import { SubmitHandler, useForm } from "react-hook-form"; -import { LoginSchema, LoginType } from "types"; - -const Login = () => { - const { - register, - handleSubmit, - formState: { errors }, - } = useForm({ - resolver: zodResolver(LoginSchema), - }); - - const auth = useAuthentication(); - const { addErrorAlert } = useAlertQueue(); - - const [useSpinner, setUseSpinner] = useState(false); - const [showPassword, setShowPassword] = useState(false); - const onSubmit: SubmitHandler = async (data: LoginType) => { - // TODO: Add an api endpoint to send the credentials details to backend. - console.log(data); - }; - - const handleGithubSubmit = async ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - - const { data, error } = await auth.client.GET("/users/github/login"); - if (error) { - addErrorAlert(error); - } else { - window.open(data, "_self"); - } - }; - - useEffect(() => { - (async () => { - // Get the code from the query string to carry out OAuth login. - const search = window.location.search; - const params = new URLSearchParams(search); - const code = params.get("code"); - - if (code) { - setUseSpinner(true); - const { data, error } = await auth.client.POST("/users/github/code", { - body: { code }, - }); - - if (error) { - addErrorAlert(error); - setUseSpinner(false); - } else { - auth.login(data.api_key); - setUseSpinner(false); - } - } - })(); - }, []); - - return ( -
    - {useSpinner ? ( - - ) : ( -
    - -
    -
    - - {errors?.email && ( - {errors?.email?.message} - )} -
    -
    - - {errors?.password && ( - {errors?.password?.message} - )} -
    - setShowPassword((p) => !p)} - className="cursor-pointer" - /> -
    -
    - -
    -
    -
    - )} -
    - ); -}; - -export default Login; diff --git a/frontend/src/pages/auth/Signup.tsx b/frontend/src/pages/auth/Signup.tsx deleted file mode 100644 index c0414c60..00000000 --- a/frontend/src/pages/auth/Signup.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { zodResolver } from "@hookform/resolvers/zod"; -import { Button } from "components/ui/Button/Button"; -import CardWrapper from "components/ui/Card/CardWrapper"; -import ErrorMessage from "components/ui/ErrorMessage"; -import { Input } from "components/ui/Input/Input"; -import { useState } from "react"; -import { Eye } from "react-bootstrap-icons"; -import { SubmitHandler, useForm } from "react-hook-form"; -import { LoginType, SignUpSchema, SignupType } from "types"; - -const Signup = () => { - const [showPassword, setShowPassword] = useState(false); - const [showConfirmPassword, setShowConfirmPassword] = - useState(false); - - const { - register, - handleSubmit, - formState: { errors }, - } = useForm({ - resolver: zodResolver(SignUpSchema), - }); - - const onSubmit: SubmitHandler = async (data: LoginType) => { - // TODO: Add an api endpoint to send the credentials details to backend and email verification. - console.log(data); - }; - - return ( -
    - -
    -
    - - {errors?.email && ( - {errors?.email?.message} - )} -
    -
    - - {errors?.password && ( - {errors?.password?.message} - )} -
    - setShowPassword((p) => !p)} - className="cursor-pointer" - /> -
    -
    -
    - - {errors?.confirmPassword && ( - {errors?.confirmPassword?.message} - )} -
    - setShowConfirmPassword((p) => !p)} - className="cursor-pointer" - /> -
    -
    - -
    -
    -
    - ); -}; - -export default Signup; diff --git a/store/app/routers/listings.py b/store/app/routers/listings.py index e0cc5dc3..0c4c6e88 100644 --- a/store/app/routers/listings.py +++ b/store/app/routers/listings.py @@ -21,7 +21,7 @@ class ListListingsResponse(BaseModel): - listings: list[Listing] + listing_ids: list[str] has_next: bool = False @@ -32,15 +32,39 @@ async def list_listings( search_query: str = Query(None, description="Search query string"), ) -> ListListingsResponse: listings, has_next = await crud.get_listings(page, search_query=search_query) - return ListListingsResponse(listings=listings, has_next=has_next) + listing_ids = [listing.id for listing in listings] + return ListListingsResponse(listing_ids=listing_ids, has_next=has_next) + +class ListingInfoResponse(BaseModel): + id: str + name: str + description: str | None + child_ids: list[str] -@listings_router.get("/batch", response_model=ListListingsResponse) -async def get_batch( + +class GetBatchListingsResponse(BaseModel): + listings: list[ListingInfoResponse] + + +@listings_router.get("/batch", response_model=GetBatchListingsResponse) +async def get_batch_listing_info( crud: Annotated[Crud, Depends(Crud.get)], ids: list[str] = Query(description="List of part ids"), -) -> ListListingsResponse: - return ListListingsResponse(listings=await crud._get_item_batch(ids, Listing)) +) -> GetBatchListingsResponse: + print("IDs:", ids) + listings = await crud._get_item_batch(ids, Listing) + return GetBatchListingsResponse( + listings=[ + ListingInfoResponse( + id=listing.id, + name=listing.name, + description=listing.description, + child_ids=listing.child_ids, + ) + for listing in listings + ] + ) class DumpListingsResponse(BaseModel): @@ -62,7 +86,8 @@ async def list_my_listings( search_query: str = Query(None, description="Search query string"), ) -> ListListingsResponse: listings, has_next = await crud.get_user_listings(user.id, page, search_query=search_query) - return ListListingsResponse(listings=listings, has_next=has_next) + listing_ids = [listing.id for listing in listings] + return ListListingsResponse(listing_ids=listing_ids, has_next=has_next) class NewListingRequest(BaseModel): diff --git a/store/app/routers/users.py b/store/app/routers/users.py index bd589e93..5212941a 100644 --- a/store/app/routers/users.py +++ b/store/app/routers/users.py @@ -4,7 +4,7 @@ from email.utils import parseaddr as parse_email_address from typing import Annotated, Literal, overload -from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response, status +from fastapi import APIRouter, Depends, HTTPException, Query, Request, status from fastapi.security.utils import get_authorization_scheme_param from pydantic.main import BaseModel as PydanticBaseModel @@ -166,7 +166,6 @@ async def delete_user_endpoint( async def logout_user_endpoint( token: Annotated[str, Depends(get_request_api_key_id)], crud: Annotated[Crud, Depends(Crud.get)], - response: Response, ) -> bool: await crud.delete_api_key(token) return True From 4882f5b8969d7ca0a7d72f6ed0f34bf1bdc4b08d Mon Sep 17 00:00:00 2001 From: Benjamin Bolte Date: Wed, 31 Jul 2024 16:41:11 -0700 Subject: [PATCH 2/3] nav buttons --- frontend/src/pages/Listings.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/frontend/src/pages/Listings.tsx b/frontend/src/pages/Listings.tsx index 9f576f2a..e9970028 100644 --- a/frontend/src/pages/Listings.tsx +++ b/frontend/src/pages/Listings.tsx @@ -65,6 +65,22 @@ const Listings = () => { onSearch={handleSearchInputEnterKey} /> + {pageNumber > 1 && ( + + )} + {moreListings && ( + + )} ); }; From 48957c6270772f73ba0b134b367297a2bb764529 Mon Sep 17 00:00:00 2001 From: Benjamin Bolte Date: Wed, 31 Jul 2024 16:45:55 -0700 Subject: [PATCH 3/3] headers + listings --- frontend/src/components/ui/Header.tsx | 2 +- tests/test_listings.py | 16 ++++++---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/ui/Header.tsx b/frontend/src/components/ui/Header.tsx index 64484759..bd15f8e0 100644 --- a/frontend/src/components/ui/Header.tsx +++ b/frontend/src/components/ui/Header.tsx @@ -7,7 +7,7 @@ interface HeaderProps { const Header = ({ label }: HeaderProps) => { return (
    -

    RoboList

    +

    Robolist

    {label &&

    {label}

    }
    ); diff --git a/tests/test_listings.py b/tests/test_listings.py index 53b086cf..f2b96169 100644 --- a/tests/test_listings.py +++ b/tests/test_listings.py @@ -53,15 +53,11 @@ async def test_listings(app_client: AsyncClient, tmpdir: Path) -> None: ) assert response.status_code == status.HTTP_200_OK, response.json() data = response.json() - items, has_next = data["listings"], data["has_next"] + items, has_next = data["listing_ids"], data["has_next"] assert len(items) == 1 assert not has_next - - # Checks the item. - item = data["listings"][0] - assert item["name"] == "test listing" - assert item["description"] == "test description" - assert item["child_ids"] == [] + listing_ids = data["listing_ids"] + assert listing_ids == [listing_id] # Checks my own listings. response = await app_client.get( @@ -71,12 +67,12 @@ async def test_listings(app_client: AsyncClient, tmpdir: Path) -> None: ) assert response.status_code == status.HTTP_200_OK, response.json() data = response.json() - items, has_next = data["listings"], data["has_next"] + items, has_next = data["listing_ids"], data["has_next"] assert len(items) == 1 assert not has_next # Gets the listing by ID. - listing_id = item["id"] + listing_id = listing_ids[0] response = await app_client.get(f"/listings/{listing_id}", headers=auth_headers) assert response.status_code == status.HTTP_200_OK, response.json() @@ -100,6 +96,6 @@ async def test_listings(app_client: AsyncClient, tmpdir: Path) -> None: ) assert response.status_code == status.HTTP_200_OK, response.json() data = response.json() - items, has_next = data["listings"], data["has_next"] + items, has_next = data["listing_ids"], data["has_next"] assert len(items) == 0 assert not has_next