From 8550b882ec2d9f38395327afe4c39b2903ad2c2c Mon Sep 17 00:00:00 2001 From: Benjamin Bolte Date: Sat, 9 Nov 2024 13:09:43 -0800 Subject: [PATCH 1/4] use typed routes --- frontend/package-lock.json | 31 ++++ frontend/package.json | 1 + frontend/src/App.tsx | 134 ++++++++++++------ frontend/src/components/auth/SignupForm.tsx | 5 +- frontend/src/components/footer/Footer.tsx | 9 +- .../listing/ListingArtifactRenderer.tsx | 3 +- .../listing/artifacts/TgzArtifact.tsx | 3 +- .../src/components/listings/ListingGrid.tsx | 6 +- .../src/components/listings/MyListingGrid.tsx | 6 +- .../src/components/listings/UpvotedGrid.tsx | 6 +- frontend/src/components/nav/Navbar.tsx | 30 ++-- frontend/src/components/pages/LinkRobot.tsx | 5 + .../src/components/terminal/RobotCard.tsx | 10 +- frontend/src/lib/types/routes.ts | 83 +++++++++++ 14 files changed, 261 insertions(+), 71 deletions(-) create mode 100644 frontend/src/components/pages/LinkRobot.tsx create mode 100644 frontend/src/lib/types/routes.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 113022f4..acb33816 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -59,6 +59,7 @@ "react-markdown": "^9.0.1", "react-masonry-css": "^1.0.16", "react-router-dom": "6.25.1", + "react-router-typesafe-routes": "^1.2.2", "react-use-websocket": "^4.8.1", "remark-gfm": "^4.0.0", "tailwind-merge": "^2.5.2", @@ -10001,6 +10002,36 @@ "react-dom": ">=16.8" } }, + "node_modules/react-router-typesafe-routes": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/react-router-typesafe-routes/-/react-router-typesafe-routes-1.2.2.tgz", + "integrity": "sha512-aUTUO0XzEhevQTNACv82SJVRbUMnGCrQIecRwKWLRNt2oKXs01kW4nIJbRV8xwAvv9PR4WKM2Iifd84DiOr73w==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8", + "react-router-dom": "^6.7.0", + "react-router-native": "^6.7.0", + "yup": "^1.0.0", + "zod": "^3.0.0" + }, + "peerDependenciesMeta": { + "react-router-dom": { + "optional": true + }, + "react-router-native": { + "optional": true + }, + "yup": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/react-side-effect": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 35aa2bb7..17849b3b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -74,6 +74,7 @@ "react-markdown": "^9.0.1", "react-masonry-css": "^1.0.16", "react-router-dom": "6.25.1", + "react-router-typesafe-routes": "^1.2.2", "react-use-websocket": "^4.8.1", "remark-gfm": "^4.0.0", "tailwind-merge": "^2.5.2", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5807698b..3f91712a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,6 +8,7 @@ import PendoInitializer from "@/components/PendoInitializer"; import { ScrollToTop } from "@/components/ScrollToTop"; import SprigInitializer from "@/components/SprigInitializer"; import Footer from "@/components/footer/Footer"; +import GDPRBanner from "@/components/gdpr/gdprbanner"; import { FeaturedListingsProvider } from "@/components/listing/FeaturedListings"; import Navbar from "@/components/nav/Navbar"; import APIKeys from "@/components/pages/APIKeys"; @@ -15,6 +16,8 @@ import About from "@/components/pages/About"; import Account from "@/components/pages/Account"; import Browse from "@/components/pages/Browse"; import Create from "@/components/pages/Create"; +import DeleteConnect from "@/components/pages/DeleteConnect"; +import DownloadsPage from "@/components/pages/Download"; import EmailSignup from "@/components/pages/EmailSignup"; import FileBrowser from "@/components/pages/FileBrowser"; import Home from "@/components/pages/Home"; @@ -22,23 +25,21 @@ import Listing from "@/components/pages/Listing"; import Login from "@/components/pages/Login"; import Logout from "@/components/pages/Logout"; import NotFound from "@/components/pages/NotFound"; +import OrderSuccess from "@/components/pages/OrderSuccess"; +import OrdersPage from "@/components/pages/Orders"; +import Playground from "@/components/pages/Playground"; +import PrivacyPolicy from "@/components/pages/PrivacyPolicy"; import Profile from "@/components/pages/Profile"; +import ResearchPage from "@/components/pages/ResearchPage"; import SellerDashboard from "@/components/pages/SellerDashboard"; import Signup from "@/components/pages/Signup"; +import Terminal from "@/components/pages/Terminal"; +import TermsOfService from "@/components/pages/TermsOfService"; import { AlertQueue, AlertQueueProvider } from "@/hooks/useAlertQueue"; import { AuthenticationProvider } from "@/hooks/useAuth"; +import ROUTES from "@/lib/types/routes"; -import GDPRBanner from "./components/gdpr/gdprbanner"; -import DeleteConnect from "./components/pages/DeleteConnect"; -import DownloadsPage from "./components/pages/Download"; -import OrderSuccess from "./components/pages/OrderSuccess"; -import OrdersPage from "./components/pages/Orders"; -import Playground from "./components/pages/Playground"; -import PrivacyPolicy from "./components/pages/PrivacyPolicy"; -import ResearchPage from "./components/pages/ResearchPage"; import SellerOnboarding from "./components/pages/SellerOnboarding"; -import Terminal from "./components/pages/Terminal"; -import TermsOfService from "./components/pages/TermsOfService"; const App = () => { return ( @@ -56,55 +57,104 @@ const App = () => {
- } /> + } /> - } /> - - } /> - } /> - } /> - } /> + {/* Playground */} } + path={ROUTES.PLAYGROUND.path} + element={} /> - } /> - } /> - } /> - } /> - } /> - } /> + {/* General pages */} + } /> } + path={ROUTES.DOWNLOADS.path} + element={} + /> + } + /> + } + /> + } /> - } /> - } /> - } /> - } /> + {/* Account */} + } + /> + } /> + } /> + } /> + } + /> + } /> + {/* Listings */} + } /> + } /> } + path={ROUTES.LISTING.path} + element={} /> } + path={ROUTES.PROFILE.path} + element={} /> } + path={ROUTES.FILE.path} + element={} /> - } /> - } /> + {/* Seller */} + + } + /> + } + /> + } + /> + - } /> - } /> + {/* Orders */} + } + /> + } + /> - } /> + {/* Terminal */} + } + /> + } + /> + + {/* Not found */} + } + /> } /> diff --git a/frontend/src/components/auth/SignupForm.tsx b/frontend/src/components/auth/SignupForm.tsx index f0654457..0dec1c41 100644 --- a/frontend/src/components/auth/SignupForm.tsx +++ b/frontend/src/components/auth/SignupForm.tsx @@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button"; import { useAlertQueue } from "@/hooks/useAlertQueue"; import { useAuthentication } from "@/hooks/useAuth"; import { SignUpSchema, SignupType } from "@/lib/types"; +import ROUTES from "@/lib/types/routes"; import { zodResolver } from "@hookform/resolvers/zod"; import zxcvbn from "zxcvbn"; @@ -93,11 +94,11 @@ const SignupForm: React.FC = ({ signupTokenId }) => { {/* TOS Text */}
By signing up, you agree to our
- + terms and conditions {" "} and{" "} - + privacy policy . diff --git a/frontend/src/components/footer/Footer.tsx b/frontend/src/components/footer/Footer.tsx index 7c0ed791..3c607e74 100644 --- a/frontend/src/components/footer/Footer.tsx +++ b/frontend/src/components/footer/Footer.tsx @@ -4,6 +4,7 @@ import { Link, useLocation } from "react-router-dom"; import Logo from "@/components/Logo"; import SocialLink from "@/components/footer/SocialLink"; +import ROUTES from "@/lib/types/routes"; const Footer = () => { const location = useLocation(); @@ -64,19 +65,19 @@ const Footer = () => {

Company

- + About - + Blog

Legal

- + Terms of Service - + Privacy Policy
diff --git a/frontend/src/components/listing/ListingArtifactRenderer.tsx b/frontend/src/components/listing/ListingArtifactRenderer.tsx index 6cebbc49..1f316614 100644 --- a/frontend/src/components/listing/ListingArtifactRenderer.tsx +++ b/frontend/src/components/listing/ListingArtifactRenderer.tsx @@ -3,6 +3,7 @@ import { Link } from "react-router-dom"; import placeholder from "@/components/listing/pics/placeholder.jpg"; import { Artifact } from "@/components/listing/types"; +import ROUTES from "@/lib/types/routes"; interface Props { artifact: Artifact; @@ -22,7 +23,7 @@ const ListingArtifactRenderer = ({ artifact }: Props) => { return (
diff --git a/frontend/src/components/listing/artifacts/TgzArtifact.tsx b/frontend/src/components/listing/artifacts/TgzArtifact.tsx index 13b55d52..b5ffc968 100644 --- a/frontend/src/components/listing/artifacts/TgzArtifact.tsx +++ b/frontend/src/components/listing/artifacts/TgzArtifact.tsx @@ -11,6 +11,7 @@ import { Link, useNavigate } from "react-router-dom"; import { Button } from "@/components/ui/button"; import { components } from "@/gen/api"; +import ROUTES from "@/lib/types/routes"; import { cx } from "class-variance-authority"; type SingleArtifactResponse = components["schemas"]["SingleArtifactResponse"]; @@ -84,7 +85,7 @@ const CodeInstructions = ({ artifactId }: { artifactId: string }) => {
  • Configure your{" "} - + API key
  • diff --git a/frontend/src/components/listings/ListingGrid.tsx b/frontend/src/components/listings/ListingGrid.tsx index 63d5c60e..b8b0a6d1 100644 --- a/frontend/src/components/listings/ListingGrid.tsx +++ b/frontend/src/components/listings/ListingGrid.tsx @@ -6,6 +6,7 @@ import Spinner from "@/components/ui/Spinner"; import { paths } from "@/gen/api"; import { useAlertQueue } from "@/hooks/useAlertQueue"; import { useAuthentication } from "@/hooks/useAuth"; +import ROUTES from "@/lib/types/routes"; import ListingGridSkeleton from "./ListingGridSkeleton"; @@ -65,7 +66,10 @@ const ListingGrid = (props: ListingGridProps) => {
    {listingInfos.map((info) => ( {listingDetails === null ? ( diff --git a/frontend/src/components/listings/MyListingGrid.tsx b/frontend/src/components/listings/MyListingGrid.tsx index bfb777ca..cb628b60 100644 --- a/frontend/src/components/listings/MyListingGrid.tsx +++ b/frontend/src/components/listings/MyListingGrid.tsx @@ -6,6 +6,7 @@ import Spinner from "@/components/ui/Spinner"; import { paths } from "@/gen/api"; import { useAlertQueue } from "@/hooks/useAlertQueue"; import { useAuthentication } from "@/hooks/useAuth"; +import ROUTES from "@/lib/types/routes"; type ListingInfo = { id: string; @@ -81,7 +82,10 @@ const MyListingGrid = ({ userId }: MyListingGridProps) => {
    {listingInfos.map((info) => ( {
    {listingInfos.map((info) => ( { const technicalItems = [ { name: "Builds", - path: "/browse", + path: ROUTES.BROWSE.path, icon: , isExternal: false, }, { name: "Downloads", - path: "/downloads", + path: ROUTES.DOWNLOADS.path, icon: , isExternal: false, }, { name: "Playground", - path: "/playground", + path: ROUTES.PLAYGROUND.path, icon: , isExternal: false, }, { name: "Research", - path: "/research", + path: ROUTES.RESEARCH.path, icon: , isExternal: false, }, @@ -126,7 +127,10 @@ const Navbar = () => { }; const handleFeaturedClick = (username: string, slug: string | null) => { - const path = `/item/${username}/${slug}`; + const path = ROUTES.LISTING.buildPath({ + username, + slug: slug || "", + }); if (location.pathname !== path) { navigate(path, { replace: true }); } @@ -138,7 +142,7 @@ const Navbar = () => {
    Account Logout @@ -259,13 +259,13 @@ const Navbar = () => { ) : ( <> Sign In Sign Up diff --git a/frontend/src/components/pages/LinkRobot.tsx b/frontend/src/components/pages/LinkRobot.tsx new file mode 100644 index 00000000..66568512 --- /dev/null +++ b/frontend/src/components/pages/LinkRobot.tsx @@ -0,0 +1,5 @@ +const LinkRobot = () => { + return
    LinkRobot
    ; +}; + +export default LinkRobot; diff --git a/frontend/src/components/terminal/RobotCard.tsx b/frontend/src/components/terminal/RobotCard.tsx index b70f7460..7a74a9b9 100644 --- a/frontend/src/components/terminal/RobotCard.tsx +++ b/frontend/src/components/terminal/RobotCard.tsx @@ -5,6 +5,7 @@ import { Link } from "react-router-dom"; import { SingleRobotResponse } from "@/components/terminal/types"; import { Card } from "@/components/ui/Card"; import { Button } from "@/components/ui/button"; +import ROUTES from "@/lib/types/routes"; import { formatDate } from "@/lib/utils/formatDate"; import { DeleteRobotModal } from "../modals/DeleteRobotModal"; @@ -22,7 +23,7 @@ export default function RobotCard({ robot, onDeleteRobot }: RobotCardProps) {
    @@ -60,7 +61,10 @@ export default function RobotCard({ robot, onDeleteRobot }: RobotCardProps) { position="bottom" > @@ -84,7 +88,7 @@ export default function RobotCard({ robot, onDeleteRobot }: RobotCardProps) { position="bottom" > diff --git a/frontend/src/lib/types/routes.ts b/frontend/src/lib/types/routes.ts new file mode 100644 index 00000000..6c6872ff --- /dev/null +++ b/frontend/src/lib/types/routes.ts @@ -0,0 +1,83 @@ +import { number, route, string } from "react-router-typesafe-routes/dom"; + +const ROUTES = { + HOME: route(""), + PLAYGROUND: route("playground"), + + // General pages + ABOUT: route("about"), + DOWNLOADS: route("downloads"), + RESEARCH: route("research"), + TOS: route("tos"), + PRIVACY: route("privacy"), + + // Account routes + ACCOUNT: route("account"), + LOGIN: route("login"), + LOGOUT: route("logout"), + SIGNUP: route( + "signup", + {}, + { + EMAIL: route(":id", { params: { id: string().defined() } }), + }, + ), + KEYS: route("keys"), + + // Listings + CREATE: route("create"), + BROWSE: route("browse/:page?", { + params: { page: number() }, + }), + LISTING: route("item/:username/:slug", { + params: { + username: string().defined(), + slug: string().defined(), + }, + }), + PROFILE: route("profile/:id?", { + params: { id: string() }, + }), + FILE: route("file/:artifactId", { + params: { artifactId: string().defined() }, + }), + + // Seller routes + SELL: route( + "sell", + {}, + { + ONBOARDING: route("onboarding"), + DASHBOARD: route("dashboard"), + DELETE: route("delete"), + }, + ), + + // Orders + ORDER: route( + "order", + {}, + { + SUCCESS: route("success"), + }, + ), + ORDERS: route("orders"), + + // Terminal + // TERMINAL: route("terminal/:id?", { + // params: { id: string() }, + // }), + TERMINAL: route("terminal", + {}, + { + WITH_ID: route(":id", { + params: { id: string() }, + }), + }, + ), + + // Not found + NOT_FOUND: route("404"), +}; + +export default ROUTES; From 8a4ac29113404710072d5bbb040b3e861514f688 Mon Sep 17 00:00:00 2001 From: Benjamin Bolte Date: Sat, 9 Nov 2024 13:10:10 -0800 Subject: [PATCH 2/4] asdf --- store/app/routers/stripe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/store/app/routers/stripe.py b/store/app/routers/stripe.py index 0b87d08b..abc549f7 100644 --- a/store/app/routers/stripe.py +++ b/store/app/routers/stripe.py @@ -275,7 +275,7 @@ async def create_checkout_session( ], automatic_tax={"enabled": True}, mode="payment", - success_url=f"{settings.site.homepage}/success?session_id={{CHECKOUT_SESSION_ID}}", + success_url=f"{settings.site.homepage}/order/success?session_id={{CHECKOUT_SESSION_ID}}", cancel_url=f"{settings.site.homepage}{cancel_url}", client_reference_id=user.id, metadata={ From 4ee080a753644605751beb620503b695f52cd074 Mon Sep 17 00:00:00 2001 From: Benjamin Bolte Date: Sat, 9 Nov 2024 13:52:59 -0800 Subject: [PATCH 3/4] update to use typed routes everywhere --- frontend/src/App.tsx | 46 +++++++------ frontend/src/components/auth/SignupForm.tsx | 2 +- .../listing/ListingDeleteButton.tsx | 3 +- .../components/listing/ListingMetadata.tsx | 3 +- .../listing/artifacts/TgzArtifact.tsx | 2 +- .../components/modals/RegisterRobotModal.tsx | 3 +- frontend/src/components/nav/Navbar.tsx | 2 +- frontend/src/components/pages/Browse.tsx | 3 +- frontend/src/components/pages/Create.tsx | 8 ++- .../src/components/pages/DeleteConnect.tsx | 7 +- frontend/src/components/pages/EmailSignup.tsx | 8 ++- frontend/src/components/pages/FileBrowser.tsx | 15 +++-- frontend/src/components/pages/Home.tsx | 66 +------------------ frontend/src/components/pages/Listing.tsx | 5 +- frontend/src/components/pages/Orders.tsx | 6 +- frontend/src/components/pages/Profile.tsx | 25 ++++--- .../src/components/pages/SellerDashboard.tsx | 5 +- .../src/components/pages/SellerOnboarding.tsx | 7 +- frontend/src/components/pages/Terminal.tsx | 5 +- .../src/components/stripe/CheckoutButton.tsx | 5 +- .../components/terminal/TerminalAllRobots.tsx | 3 +- .../terminal/TerminalSingleRobot.tsx | 12 +++- frontend/src/hooks/useAuth.tsx | 5 +- frontend/src/lib/types/routes.ts | 21 +++--- 24 files changed, 131 insertions(+), 136 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3f91712a..040e3452 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -32,6 +32,7 @@ import PrivacyPolicy from "@/components/pages/PrivacyPolicy"; import Profile from "@/components/pages/Profile"; import ResearchPage from "@/components/pages/ResearchPage"; import SellerDashboard from "@/components/pages/SellerDashboard"; +import SellerOnboarding from "@/components/pages/SellerOnboarding"; import Signup from "@/components/pages/Signup"; import Terminal from "@/components/pages/Terminal"; import TermsOfService from "@/components/pages/TermsOfService"; @@ -39,8 +40,6 @@ import { AlertQueue, AlertQueueProvider } from "@/hooks/useAlertQueue"; import { AuthenticationProvider } from "@/hooks/useAuth"; import ROUTES from "@/lib/types/routes"; -import SellerOnboarding from "./components/pages/SellerOnboarding"; - const App = () => { return ( @@ -97,44 +96,53 @@ const App = () => { element={} /> } /> + } + /> {/* Listings */} - } /> - } /> + + } + /> + } + /> + } /> - } - /> } /> {/* Seller */} - + } + > } /> } - /> - } /> {/* Orders */} - } - /> + + } + /> + } diff --git a/frontend/src/components/auth/SignupForm.tsx b/frontend/src/components/auth/SignupForm.tsx index 0dec1c41..d002d06b 100644 --- a/frontend/src/components/auth/SignupForm.tsx +++ b/frontend/src/components/auth/SignupForm.tsx @@ -57,7 +57,7 @@ const SignupForm: React.FC = ({ signupTokenId }) => { addErrorAlert(error); } else { addAlert("Registration successful! You can now log in.", "success"); - navigate("/login"); + navigate(ROUTES.LOGIN.path); // Sign user in automatically? } } catch (err) { diff --git a/frontend/src/components/listing/ListingDeleteButton.tsx b/frontend/src/components/listing/ListingDeleteButton.tsx index 43f1a0fd..8919c43a 100644 --- a/frontend/src/components/listing/ListingDeleteButton.tsx +++ b/frontend/src/components/listing/ListingDeleteButton.tsx @@ -6,6 +6,7 @@ import Modal from "@/components/ui/Modal"; import { Button } from "@/components/ui/button"; import { useAlertQueue } from "@/hooks/useAlertQueue"; import { useAuthentication } from "@/hooks/useAuth"; +import ROUTES from "@/lib/types/routes"; interface Props { listingId: string; @@ -37,7 +38,7 @@ const ListingDeleteButton = (props: Props) => { setDeleting(false); } else { addAlert("Listing was deleted successfully", "success"); - navigate("/browse"); + navigate(ROUTES.LISTINGS.BROWSE.path); } }; diff --git a/frontend/src/components/listing/ListingMetadata.tsx b/frontend/src/components/listing/ListingMetadata.tsx index a86858a6..45c6ffff 100644 --- a/frontend/src/components/listing/ListingMetadata.tsx +++ b/frontend/src/components/listing/ListingMetadata.tsx @@ -10,6 +10,7 @@ import { useNavigate } from "react-router-dom"; import { useAlertQueue } from "@/hooks/useAlertQueue"; import { useAuthentication } from "@/hooks/useAuth"; +import ROUTES from "@/lib/types/routes"; interface Props { listingId: string; @@ -110,7 +111,7 @@ const ListingMetadata = ({
    diff --git a/frontend/src/components/pages/Profile.tsx b/frontend/src/components/pages/Profile.tsx index a4460c14..0720e951 100644 --- a/frontend/src/components/pages/Profile.tsx +++ b/frontend/src/components/pages/Profile.tsx @@ -1,6 +1,8 @@ import { useEffect, useState } from "react"; -import { useNavigate, useParams } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; +import { useTypedParams } from "react-router-typesafe-routes/dom"; +import MyListingGrid from "@/components/listings/MyListingGrid"; import UpvotedGrid from "@/components/listings/UpvotedGrid"; import { Card, CardContent, CardHeader } from "@/components/ui/Card"; import { Input, TextArea } from "@/components/ui/Input/Input"; @@ -11,12 +13,11 @@ import { paths } from "@/gen/api"; import { useAlertQueue } from "@/hooks/useAlertQueue"; import { useAuthentication } from "@/hooks/useAuth"; import { useDebounce } from "@/hooks/useDebounce"; +import ROUTES from "@/lib/types/routes"; import { isValidUsername } from "@/lib/utils/validation"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@radix-ui/react-tabs"; import { format } from "date-fns"; -import MyListingGrid from "../listings/MyListingGrid"; - type UserResponse = paths["/users/public/{id}"]["get"]["responses"][200]["content"]["application/json"]; @@ -214,7 +215,10 @@ export const RenderProfile = (props: RenderProfileProps) => {
    {!isEditing && canEdit && (
    -
    - {!user.stripe_connect_account_id ? (