From fafe47bbcf8d43242990d314ed0b1781791029ab Mon Sep 17 00:00:00 2001 From: Ivan <45982459+ivntsng@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:39:00 -0700 Subject: [PATCH] Upvoted listings (#433) * 338: View upvoted listings * 338: View upvoted listings * Updated UI to match theme * Resolved linting issue --------- Co-authored-by: casper --- .../src/components/listings/UpvotedGrid.tsx | 142 + frontend/src/components/pages/Profile.tsx | 28 +- frontend/src/gen/api.ts | 56 + src/gen/api.ts | 2534 +++++++++++++++++ store/app/crud/listings.py | 24 + store/app/routers/listings.py | 18 + 6 files changed, 2800 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/listings/UpvotedGrid.tsx create mode 100644 src/gen/api.ts diff --git a/frontend/src/components/listings/UpvotedGrid.tsx b/frontend/src/components/listings/UpvotedGrid.tsx new file mode 100644 index 00000000..1a03c6f8 --- /dev/null +++ b/frontend/src/components/listings/UpvotedGrid.tsx @@ -0,0 +1,142 @@ +import { useEffect, useState } from "react"; +import { FaChevronLeft, FaChevronRight } from "react-icons/fa"; +import { Link, useNavigate, useSearchParams } from "react-router-dom"; + +import ListingGridCard from "@/components/listings/ListingGridCard"; +import Spinner from "@/components/ui/Spinner"; +import { paths } from "@/gen/api"; +import { useAlertQueue } from "@/hooks/useAlertQueue"; +import { useAuthentication } from "@/hooks/useAuth"; + +type ListingInfo = + paths["/listings/batch"]["get"]["responses"][200]["content"]["application/json"]["listings"][number]; + +const UpvotedGrid = () => { + const auth = useAuthentication(); + const { addErrorAlert } = useAlertQueue(); + const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); + + const page = searchParams.get("page"); + const pageNumber = parseInt(page || "1", 10); + if (isNaN(pageNumber) || pageNumber < 1) { + navigate("/404"); + } + + const [listingIds, setListingIds] = useState(null); + const [hasMore, setHasMore] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [listingInfo, setListingInfo] = useState(null); + + useEffect(() => { + fetchUpvotedListings(); + }, [pageNumber]); + + const fetchUpvotedListings = async () => { + setIsLoading(true); + setListingIds(null); + setListingInfo(null); + + const { data, error } = await auth.client.GET("/listings/upvotes", { + params: { + query: { + page: pageNumber, + }, + }, + }); + if (error) { + addErrorAlert(error); + } else { + setListingIds(data.upvoted_listing_ids); + setHasMore(data.has_more); + } + setIsLoading(false); + }; + + useEffect(() => { + if (listingIds && listingIds.length > 0) { + fetchListingDetails(); + } + }, [listingIds]); + + const fetchListingDetails = async () => { + if (!listingIds) { + return; + } + + const { data, error } = await auth.client.GET("/listings/batch", { + params: { + query: { + ids: listingIds, + }, + }, + }); + + if (error) { + addErrorAlert(error); + } else { + setListingInfo(data.listings); + } + }; + + const prevButton = pageNumber > 1; + const nextButton = hasMore; + + return ( + <> + {isLoading ? ( +
+ +
+ ) : ( + <> + {listingInfo && listingInfo.length > 0 ? ( +
+
+ {listingInfo.map((listing) => ( + + + + ))} +
+
+ + +
+
+ ) : ( +

You have not upvoted any listings

+ )} + + )} + + ); +}; + +export default UpvotedGrid; diff --git a/frontend/src/components/pages/Profile.tsx b/frontend/src/components/pages/Profile.tsx index c9cfc6b8..623a1a2f 100644 --- a/frontend/src/components/pages/Profile.tsx +++ b/frontend/src/components/pages/Profile.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import ListingGrid from "@/components/listings/ListingGrid"; +import UpvotedGrid from "@/components/listings/UpvotedGrid"; import { Card, CardContent, CardHeader } from "@/components/ui/Card"; import { Input, TextArea } from "@/components/ui/Input/Input"; import Spinner from "@/components/ui/Spinner"; @@ -9,6 +10,7 @@ import { Button } from "@/components/ui/button"; import { paths } from "@/gen/api"; import { useAlertQueue } from "@/hooks/useAlertQueue"; import { useAuthentication } from "@/hooks/useAuth"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@radix-ui/react-tabs"; import { format } from "date-fns"; type UserResponse = @@ -183,13 +185,35 @@ export const RenderProfile = (props: RenderProfileProps) => { )} -

Listings

- {listingIds && } +
+ + + + Overview + + + Upvoted + + + + {listingIds && } + + + + + +
diff --git a/frontend/src/gen/api.ts b/frontend/src/gen/api.ts index fc8bcc0f..88a03b75 100644 --- a/frontend/src/gen/api.ts +++ b/frontend/src/gen/api.ts @@ -364,6 +364,23 @@ export interface paths { patch?: never; trace?: never; }; + "/listings/upvotes": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Upvoted Listings */ + get: operations["get_upvoted_listings_listings_upvotes_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/listings/{id}": { parameters: { query?: never; @@ -1248,6 +1265,13 @@ export interface components { /** Artifacts */ artifacts: components["schemas"]["SingleArtifactResponse"][]; }; + /** UpvotedListingsResponse */ + UpvotedListingsResponse: { + /** Upvoted Listing Ids */ + upvoted_listing_ids: string[]; + /** Has More */ + has_more: boolean; + }; /** UserInfoResponseItem */ UserInfoResponseItem: { /** Id */ @@ -1963,6 +1987,38 @@ export interface operations { }; }; }; + get_upvoted_listings_listings_upvotes_get: { + parameters: { + query?: { + /** @description Page number for pagination */ + page?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UpvotedListingsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; get_listing_listings__id__get: { parameters: { query?: never; diff --git a/src/gen/api.ts b/src/gen/api.ts new file mode 100644 index 00000000..ce8362dd --- /dev/null +++ b/src/gen/api.ts @@ -0,0 +1,2534 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Read Root */ + get: operations["read_root__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/artifacts/url/{artifact_type}/{listing_id}/{name}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Artifact Url */ + get: operations["artifact_url_artifacts_url__artifact_type___listing_id___name__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/artifacts/info/{artifact_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Artifact Info */ + get: operations["get_artifact_info_artifacts_info__artifact_id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/artifacts/list/{listing_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List Artifacts */ + get: operations["list_artifacts_artifacts_list__listing_id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/artifacts/upload/{listing_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Upload */ + post: operations["upload_artifacts_upload__listing_id__post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/artifacts/edit/{artifact_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Edit Artifact */ + put: operations["edit_artifact_artifacts_edit__artifact_id__put"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/artifacts/delete/{artifact_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** Delete Artifact */ + delete: operations["delete_artifact_artifacts_delete__artifact_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/email/signup/create": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create Signup Token + * @description Creates a signup token and emails it to the user. + */ + post: operations["create_signup_token_email_signup_create_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/email/signup/get/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Signup Token */ + get: operations["get_signup_token_email_signup_get__id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/email/signup/delete/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** Delete Signup Token */ + delete: operations["delete_signup_token_email_signup_delete__id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/keys/new": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** New Key */ + post: operations["new_key_keys_new_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/keys/list": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List Keys */ + get: operations["list_keys_keys_list_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/keys/delete/{key}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** Delete Key */ + delete: operations["delete_key_keys_delete__key__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/listings/search": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List Listings */ + get: operations["list_listings_listings_search_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/listings/batch": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Batch Listing Info */ + get: operations["get_batch_listing_info_listings_batch_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/listings/dump": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Dump Listings */ + get: operations["dump_listings_listings_dump_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/listings/user/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List User Listings */ + get: operations["list_user_listings_listings_user__id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/listings/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List My Listings */ + get: operations["list_my_listings_listings_me_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/listings/add": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Add Listing */ + post: operations["add_listing_listings_add_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/listings/delete/{listing_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** Delete Listing */ + delete: operations["delete_listing_listings_delete__listing_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/listings/edit/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Edit Listing */ + put: operations["edit_listing_listings_edit__id__put"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/listings/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Listing */ + get: operations["get_listing_listings__id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/listings/{id}/view": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Increment View Count */ + post: operations["increment_view_count_listings__id__view_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/listings/{id}/vote": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Vote Listing */ + post: operations["vote_listing_listings__id__vote_post"]; + /** Remove Vote */ + delete: operations["remove_vote_listings__id__vote_delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/listings/upvotes": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Upvoted Listings */ + get: operations["get_upvoted_listings_listings_upvotes_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/onshape/set/{listing_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Set Onshape Document */ + post: operations["set_onshape_document_onshape_set__listing_id__post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/onshape/pull/{listing_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Pull Onshape Document */ + get: operations["pull_onshape_document_onshape_pull__listing_id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get User Info Endpoint */ + get: operations["get_user_info_endpoint_users_me_get"]; + /** Update Profile */ + put: operations["update_profile_users_me_put"]; + post?: never; + /** Delete User Endpoint */ + delete: operations["delete_user_endpoint_users_me_delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/logout": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** Logout User Endpoint */ + delete: operations["logout_user_endpoint_users_logout_delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/signup": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Register User */ + post: operations["register_user_users_signup_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/login": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Login User */ + post: operations["login_user_users_login_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/batch": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Users Batch Endpoint */ + get: operations["get_users_batch_endpoint_users_batch_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/public/batch": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Users Public Batch Endpoint */ + get: operations["get_users_public_batch_endpoint_users_public_batch_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get User Info By Id Endpoint */ + get: operations["get_user_info_by_id_endpoint_users__id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/public/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get My Public User Info Endpoint */ + get: operations["get_my_public_user_info_endpoint_users_public_me_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/public/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Public User Info By Id Endpoint */ + get: operations["get_public_user_info_by_id_endpoint_users_public__id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/validate-api-key": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Validate Api Key Endpoint */ + get: operations["validate_api_key_endpoint_users_validate_api_key_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/github/client-id": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Github Client Id Endpoint */ + get: operations["github_client_id_endpoint_users_github_client_id_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/github/code": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Github Code + * @description Gives the user a session token upon successful github authentication and creation of user. + * + * Args: + * data: The request body, containing the code from the OAuth redirect. + * crud: The CRUD object. + * response: The response object. + * + * Returns: + * UserInfoResponse. + */ + post: operations["github_code_users_github_code_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/google/client-id": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Google Client Id Endpoint */ + get: operations["google_client_id_endpoint_users_google_client_id_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/google/login": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Google Login Endpoint */ + post: operations["google_login_endpoint_users_google_login_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/set-moderator": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Set Moderator */ + post: operations["set_moderator_users_set_moderator_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + /** ArtifactUrls */ + ArtifactUrls: { + /** Small */ + small?: string | null; + /** Large */ + large: string; + }; + /** AuthResponse */ + AuthResponse: { + /** Api Key */ + api_key: string; + }; + /** Body_pull_onshape_document_onshape_pull__listing_id__get */ + Body_pull_onshape_document_onshape_pull__listing_id__get: { + /** Suffix To Joint Effort */ + suffix_to_joint_effort?: { + [key: string]: number; + } | null; + /** Suffix To Joint Velocity */ + suffix_to_joint_velocity?: { + [key: string]: number; + } | null; + }; + /** Body_upload_artifacts_upload__listing_id__post */ + Body_upload_artifacts_upload__listing_id__post: { + /** Files */ + files: string[]; + }; + /** ClientIdResponse */ + ClientIdResponse: { + /** Client Id */ + client_id: string; + }; + /** DeleteTokenResponse */ + DeleteTokenResponse: { + /** Message */ + message: string; + }; + /** DumpListingsResponse */ + DumpListingsResponse: { + /** Listings */ + listings: components["schemas"]["Listing"][]; + }; + /** EmailSignUpRequest */ + EmailSignUpRequest: { + /** + * Email + * Format: email + */ + email: string; + }; + /** EmailSignUpResponse */ + EmailSignUpResponse: { + /** Message */ + message: string; + }; + /** GetBatchListingsResponse */ + GetBatchListingsResponse: { + /** Listings */ + listings: components["schemas"]["ListingInfoResponse"][]; + }; + /** GetListingResponse */ + GetListingResponse: { + /** Id */ + id: string; + /** Name */ + name: string; + /** Description */ + description: string | null; + /** Child Ids */ + child_ids: string[]; + /** Tags */ + tags: string[]; + /** Onshape Url */ + onshape_url: string | null; + /** Can Edit */ + can_edit: boolean; + /** Created At */ + created_at: number; + /** Views */ + views: number; + /** Score */ + score: number; + /** User Vote */ + user_vote: boolean | null; + /** Creator Id */ + creator_id: string; + /** Creator Name */ + creator_name: string | null; + }; + /** GetTokenResponse */ + GetTokenResponse: { + /** Id */ + id: string; + /** Email */ + email: string; + }; + /** GithubAuthRequest */ + GithubAuthRequest: { + /** Code */ + code: string; + }; + /** GithubAuthResponse */ + GithubAuthResponse: { + /** Api Key */ + api_key: string; + }; + /** GoogleLogin */ + GoogleLogin: { + /** Token */ + token: string; + }; + /** HTTPValidationError */ + HTTPValidationError: { + /** Detail */ + detail?: components["schemas"]["ValidationError"][]; + }; + /** KeysResponseItem */ + KeysResponseItem: { + /** Token */ + token: string; + /** Permissions */ + permissions: ("read" | "write" | "admin")[] | null; + }; + /** ListArtifactsResponse */ + ListArtifactsResponse: { + /** Artifacts */ + artifacts: components["schemas"]["SingleArtifactResponse"][]; + }; + /** ListKeysResponse */ + ListKeysResponse: { + /** Keys */ + keys: components["schemas"]["KeysResponseItem"][]; + }; + /** ListListingsResponse */ + ListListingsResponse: { + /** Listing Ids */ + listing_ids: string[]; + /** + * Has Next + * @default false + */ + has_next: boolean; + }; + /** + * Listing + * @description Defines a recursively-defined listing. + * + * Listings can have sub-listings with their component parts. They can also + * have associated user-uploaded artifacts like images and URDFs. + */ + Listing: { + /** Id */ + id: string; + /** User Id */ + user_id: string; + /** Created At */ + created_at: number; + /** Updated At */ + updated_at: number; + /** Name */ + name: string; + /** Child Ids */ + child_ids: string[]; + /** Description */ + description?: string | null; + /** Onshape Url */ + onshape_url?: string | null; + /** + * Views + * @default 0 + */ + views: number; + /** + * Upvotes + * @default 0 + */ + upvotes: number; + /** + * Downvotes + * @default 0 + */ + downvotes: number; + /** + * Score + * @default 0 + */ + score: number; + }; + /** ListingInfoResponse */ + ListingInfoResponse: { + /** Id */ + id: string; + /** Name */ + name: string; + /** Description */ + description: string | null; + /** Child Ids */ + child_ids: string[]; + /** Image Url */ + image_url: string | null; + /** Onshape Url */ + onshape_url: string | null; + /** Created At */ + created_at: number; + /** Views */ + views: number; + /** Score */ + score: number; + /** User Vote */ + user_vote: boolean | null; + }; + /** LoginRequest */ + LoginRequest: { + /** + * Email + * Format: email + */ + email: string; + /** Password */ + password: string; + }; + /** LoginResponse */ + LoginResponse: { + /** User Id */ + user_id: string; + /** Token */ + token: string; + }; + /** MyUserInfoResponse */ + MyUserInfoResponse: { + /** User Id */ + user_id: string; + /** Email */ + email: string; + /** Github Id */ + github_id: string | null; + /** Google Id */ + google_id: string | null; + /** Permissions */ + permissions: ("is_admin" | "is_mod")[] | null; + /** First Name */ + first_name: string | null; + /** Last Name */ + last_name: string | null; + /** Name */ + name: string | null; + /** Bio */ + bio: string | null; + }; + /** NewKeyRequest */ + NewKeyRequest: { + /** + * Readonly + * @default true + */ + readonly: boolean; + }; + /** NewKeyResponse */ + NewKeyResponse: { + /** User Id */ + user_id: string; + key: components["schemas"]["KeysResponseItem"]; + }; + /** NewListingRequest */ + NewListingRequest: { + /** Name */ + name: string; + /** Child Ids */ + child_ids: string[]; + /** Description */ + description: string | null; + }; + /** NewListingResponse */ + NewListingResponse: { + /** Listing Id */ + listing_id: string; + }; + /** PublicUserInfoResponseItem */ + PublicUserInfoResponseItem: { + /** Id */ + id: string; + /** Email */ + email: string; + /** Permissions */ + permissions?: ("is_admin" | "is_mod")[] | null; + /** Created At */ + created_at?: number | null; + /** Updated At */ + updated_at?: number | null; + /** First Name */ + first_name?: string | null; + /** Last Name */ + last_name?: string | null; + /** Name */ + name?: string | null; + /** Bio */ + bio?: string | null; + }; + /** PublicUsersInfoResponse */ + PublicUsersInfoResponse: { + /** Users */ + users: components["schemas"]["PublicUserInfoResponseItem"][]; + }; + /** SetModeratorRequest */ + SetModeratorRequest: { + /** User Id */ + user_id: string; + /** Is Mod */ + is_mod: boolean; + }; + /** SetRequest */ + SetRequest: { + /** Onshape Url */ + onshape_url: string | null; + }; + /** SingleArtifactResponse */ + SingleArtifactResponse: { + /** Artifact Id */ + artifact_id: string; + /** Listing Id */ + listing_id: string; + /** Name */ + name: string; + /** Artifact Type */ + artifact_type: "image" | ("urdf" | "mjcf") | ("stl" | "obj" | "dae" | "ply") | ("tgz" | "zip"); + /** Description */ + description: string | null; + /** Timestamp */ + timestamp: number; + urls: components["schemas"]["ArtifactUrls"]; + }; + /** + * SortOption + * @enum {string} + */ + SortOption: "newest" | "most_viewed" | "most_upvoted"; + /** UpdateArtifactRequest */ + UpdateArtifactRequest: { + /** Name */ + name?: string | null; + /** Description */ + description?: string | null; + }; + /** UpdateListingRequest */ + UpdateListingRequest: { + /** Name */ + name?: string | null; + /** Child Ids */ + child_ids?: string[] | null; + /** Description */ + description?: string | null; + /** Tags */ + tags?: string[] | null; + }; + /** UpdateUserRequest */ + UpdateUserRequest: { + /** Email */ + email?: string | null; + /** Password */ + password?: string | null; + /** Github Id */ + github_id?: string | null; + /** Google Id */ + google_id?: string | null; + /** First Name */ + first_name?: string | null; + /** Last Name */ + last_name?: string | null; + /** Name */ + name?: string | null; + /** Bio */ + bio?: string | null; + }; + /** UploadArtifactResponse */ + UploadArtifactResponse: { + /** Artifacts */ + artifacts: components["schemas"]["SingleArtifactResponse"][]; + }; + /** UserInfoResponseItem */ + UserInfoResponseItem: { + /** Id */ + id: string; + /** Email */ + email: string; + }; + /** + * UserPublic + * @description Defines public user model for frontend. + * + * Omits private/sesnsitive user fields. Is the return type for + * retrieving user data on frontend (for public profile pages, etc). + */ + UserPublic: { + /** Id */ + id: string; + /** Email */ + email: string; + /** Permissions */ + permissions?: ("is_admin" | "is_mod")[] | null; + /** Created At */ + created_at: number; + /** Updated At */ + updated_at?: number | null; + /** First Name */ + first_name?: string | null; + /** Last Name */ + last_name?: string | null; + /** Name */ + name?: string | null; + /** Bio */ + bio?: string | null; + }; + /** UserSignup */ + UserSignup: { + /** Signup Token Id */ + signup_token_id: string; + /** Email */ + email: string; + /** Password */ + password: string; + }; + /** ValidationError */ + ValidationError: { + /** Location */ + loc: (string | number)[]; + /** Message */ + msg: string; + /** Error Type */ + type: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + read_root__get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": boolean; + }; + }; + }; + }; + artifact_url_artifacts_url__artifact_type___listing_id___name__get: { + parameters: { + query?: { + size?: "small" | "large"; + }; + header?: never; + path: { + artifact_type: "image" | ("urdf" | "mjcf") | ("stl" | "obj" | "dae" | "ply") | ("tgz" | "zip"); + listing_id: string; + name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_artifact_info_artifacts_info__artifact_id__get: { + parameters: { + query?: never; + header?: never; + path: { + artifact_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SingleArtifactResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_artifacts_artifacts_list__listing_id__get: { + parameters: { + query?: never; + header?: never; + path: { + listing_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ListArtifactsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + upload_artifacts_upload__listing_id__post: { + parameters: { + query?: never; + header?: never; + path: { + listing_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "multipart/form-data": components["schemas"]["Body_upload_artifacts_upload__listing_id__post"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UploadArtifactResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + edit_artifact_artifacts_edit__artifact_id__put: { + parameters: { + query: { + id: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateArtifactRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": boolean; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_artifact_artifacts_delete__artifact_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + artifact_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": boolean; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + create_signup_token_email_signup_create_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["EmailSignUpRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EmailSignUpResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_signup_token_email_signup_get__id__get: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetTokenResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_signup_token_email_signup_delete__id__delete: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeleteTokenResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + new_key_keys_new_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["NewKeyRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["NewKeyResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_keys_keys_list_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ListKeysResponse"]; + }; + }; + }; + }; + delete_key_keys_delete__key__delete: { + parameters: { + query?: never; + header?: never; + path: { + key: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_listings_listings_search_get: { + parameters: { + query?: { + /** @description Page number for pagination */ + page?: number; + /** @description Search query string */ + search_query?: string; + /** @description Sort option for listings */ + sort_by?: components["schemas"]["SortOption"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ListListingsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_batch_listing_info_listings_batch_get: { + parameters: { + query: { + /** @description List of part ids */ + ids: string[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetBatchListingsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + dump_listings_listings_dump_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DumpListingsResponse"]; + }; + }; + }; + }; + list_user_listings_listings_user__id__get: { + parameters: { + query: { + /** @description Page number for pagination */ + page: number; + /** @description Search query string */ + search_query?: string; + }; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ListListingsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_my_listings_listings_me_get: { + parameters: { + query: { + /** @description Page number for pagination */ + page: number; + /** @description Search query string */ + search_query?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ListListingsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + add_listing_listings_add_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["NewListingRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["NewListingResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_listing_listings_delete__listing_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + listing_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": boolean; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + edit_listing_listings_edit__id__put: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateListingRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": boolean; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_listing_listings__id__get: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetListingResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + increment_view_count_listings__id__view_post: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + vote_listing_listings__id__vote_post: { + parameters: { + query: { + /** @description True for upvote, False for downvote */ + upvote: boolean; + }; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + remove_vote_listings__id__vote_delete: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_upvoted_listings_listings_upvotes_get: { + parameters: { + query?: { + /** @description Page number for pagination */ + page?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ListListingsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + set_onshape_document_onshape_set__listing_id__post: { + parameters: { + query?: never; + header?: never; + path: { + listing_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SetRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + pull_onshape_document_onshape_pull__listing_id__get: { + parameters: { + query?: { + token?: string | null; + default_prismatic_joint_effort?: number; + default_prismatic_joint_velocity?: number; + default_revolute_joint_effort?: number; + default_revolute_joint_velocity?: number; + voxel_size?: number; + convex_collision_meshes?: boolean; + add_mjcf?: boolean; + }; + header?: never; + path: { + listing_id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["Body_pull_onshape_document_onshape_pull__listing_id__get"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_user_info_endpoint_users_me_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MyUserInfoResponse"]; + }; + }; + }; + }; + update_profile_users_me_put: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateUserRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserPublic"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_user_endpoint_users_me_delete: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": boolean; + }; + }; + }; + }; + logout_user_endpoint_users_logout_delete: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": boolean; + }; + }; + }; + }; + register_user_users_signup_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UserSignup"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserInfoResponseItem"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + login_user_users_login_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["LoginRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LoginResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_users_batch_endpoint_users_batch_get: { + parameters: { + query: { + ids: string[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PublicUsersInfoResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_users_public_batch_endpoint_users_public_batch_get: { + parameters: { + query: { + ids: string[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PublicUsersInfoResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_user_info_by_id_endpoint_users__id__get: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserInfoResponseItem"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_my_public_user_info_endpoint_users_public_me_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserPublic"]; + }; + }; + }; + }; + get_public_user_info_by_id_endpoint_users_public__id__get: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserPublic"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + validate_api_key_endpoint_users_validate_api_key_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": boolean; + }; + }; + }; + }; + github_client_id_endpoint_users_github_client_id_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ClientIdResponse"]; + }; + }; + }; + }; + github_code_users_github_code_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GithubAuthRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GithubAuthResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + google_client_id_endpoint_users_google_client_id_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ClientIdResponse"]; + }; + }; + }; + }; + google_login_endpoint_users_google_login_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GoogleLogin"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AuthResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + set_moderator_users_set_moderator_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SetModeratorRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserPublic"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; +} diff --git a/store/app/crud/listings.py b/store/app/crud/listings.py index b0937c2c..d6778bf9 100644 --- a/store/app/crud/listings.py +++ b/store/app/crud/listings.py @@ -287,3 +287,27 @@ async def handle_vote(self, user_id: str, listing_id: str, upvote: bool | None) async def get_user_votes(self, user_id: str, listing_ids: list[str]) -> list[ListingVote]: votes = await self._get_items_from_secondary_index("user_id", user_id, ListingVote) return [vote for vote in votes if vote.listing_id in listing_ids] + + async def get_upvoted_listings(self, user_id: str, page: int = 1) -> tuple[list[Listing], bool]: + user_votes = await self._get_items_from_secondary_index( + secondary_index_name="user_id", secondary_index_value=user_id, item_class=ListingVote + ) + + upvoted_listing_ids = [vote.listing_id for vote in user_votes if vote.is_upvote] + + if not upvoted_listing_ids: + return [], False + + listings = await asyncio.gather(*(self.get_listing(listing_id) for listing_id in upvoted_listing_ids)) + + listings = [listing for listing in listings if listing is not None] + + listings.sort(key=lambda x: x.created_at, reverse=True) + + start = (page - 1) * self.PAGE_SIZE + end = start + self.PAGE_SIZE + paginated_listings = listings[start:end] + + has_more = len(upvoted_listing_ids) > end + + return paginated_listings, has_more diff --git a/store/app/routers/listings.py b/store/app/routers/listings.py index 9bb1fa27..d58e4811 100644 --- a/store/app/routers/listings.py +++ b/store/app/routers/listings.py @@ -218,6 +218,24 @@ async def edit_listing( return True +class UpvotedListingsResponse(BaseModel): + upvoted_listing_ids: list[str] + has_more: bool + + +@listings_router.get("/upvotes", response_model=UpvotedListingsResponse) +async def get_upvoted_listings( + crud: Annotated[Crud, Depends(Crud.get)], + user: Annotated[User, Depends(get_session_user_with_read_permission)], + page: int = Query(1, description="Page number for pagination"), +) -> UpvotedListingsResponse: + listings, has_more = await crud.get_upvoted_listings(user.id, page) + + upvoted_listing_ids = [listing.id for listing in listings] + + return UpvotedListingsResponse(upvoted_listing_ids=upvoted_listing_ids, has_more=has_more) + + class GetListingResponse(BaseModel): id: str name: str