SuggestedApps.tsx raw

   1  import React from "react";
   2  import { Link, useSearchParams } from "react-router";
   3  import { Badge } from "src/components/ui/badge";
   4  import {
   5    Card,
   6    CardContent,
   7    CardDescription,
   8    CardTitle,
   9  } from "src/components/ui/card";
  10  import { cn } from "src/lib/utils";
  11  import {
  12    AppStoreApp,
  13    appStoreApps,
  14    getAppStoreUrl,
  15    sortedAppStoreCategories,
  16  } from "./SuggestedAppData";
  17  
  18  function AppCard(app: AppStoreApp) {
  19    return (
  20      <Link to={getAppStoreUrl(app)}>
  21        <Card className="h-full">
  22          <CardContent>
  23            <div className="flex gap-3 items-center">
  24              <img
  25                src={app.logo}
  26                alt={`${app.title} logo`}
  27                className="inline rounded-lg size-12"
  28              />
  29              <div className="grow">
  30                <CardTitle>{app.title}</CardTitle>
  31                <CardDescription>
  32                  {app.description}
  33                  {app.legacyTitles?.length
  34                    ? ` (Previously ${app.legacyTitles.join(", ")})`
  35                    : ""}
  36                </CardDescription>
  37              </div>
  38            </div>
  39          </CardContent>
  40        </Card>
  41      </Link>
  42    );
  43  }
  44  
  45  export default function SuggestedApps() {
  46    const [searchParams] = useSearchParams();
  47    const [selectedCategories, setSelectedCategories] = React.useState<string[]>(
  48      () => {
  49        const category = searchParams.get("category");
  50        return category ? [category] : [];
  51      }
  52    );
  53  
  54    return (
  55      <>
  56        <div className="flex gap-2 flex-wrap mt-6 mb-2">
  57          {sortedAppStoreCategories.map(([categoryId, category]) => (
  58            <Badge
  59              key={categoryId}
  60              variant={
  61                selectedCategories.includes(categoryId) ? "default" : "secondary"
  62              }
  63              className={cn(
  64                "cursor-pointer",
  65                selectedCategories.includes(categoryId)
  66                  ? ""
  67                  : "border-transparent font-normal select-none"
  68              )}
  69              onClick={() =>
  70                setSelectedCategories((current) => [
  71                  ...current.filter((c) => c !== categoryId),
  72                  ...(current.includes(categoryId) ? [] : [categoryId]),
  73                ])
  74              }
  75            >
  76              {category.title}
  77            </Badge>
  78          ))}
  79        </div>
  80        <div className="flex flex-col gap-8">
  81          {sortedAppStoreCategories
  82            .filter(
  83              ([categoryId]) =>
  84                !selectedCategories.length ||
  85                selectedCategories.includes(categoryId)
  86            )
  87            .map(([categoryId, category]) => {
  88              return (
  89                <div key={categoryId} className="pt-4">
  90                  <h3 className="font-semibold text-xl">{category.title}</h3>
  91                  <div className="grid md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-3 mt-4">
  92                    {appStoreApps
  93                      .filter((app) =>
  94                        (app.categories as string[]).includes(categoryId)
  95                      )
  96                      .map((app) => (
  97                        <AppCard key={app.id} {...app} />
  98                      ))}
  99                  </div>
 100                </div>
 101              );
 102            })}
 103        </div>
 104      </>
 105    );
 106  }
 107