All files / src/packlets/project-list index.tsx

0% Statements 0/32
0% Branches 0/2
0% Functions 0/11
0% Lines 0/32

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153                                                                                                                                                                                                                                                                                                                 
/**
 * @packageDocumentation
 *
 * Home page listing saved projects. Supports opening a folder via the
 * File System API, creating a demo project, and removing projects from
 * the local index.
 */
 
import { useCallback, useState } from "react";
import { useNavigate, useLoaderData, useRevalidator } from "react-router";
import {
  Box,
  Button,
  Card,
  Flex,
  Heading,
  Text,
  DropdownMenu,
  IconButton,
  Dialog,
} from "@radix-ui/themes";
import { addProject, removeProject } from "../project-store";
import { showDirectoryPicker } from "../file-system";
import { useToast } from "../toast";
import type { Project } from "../project-store/types";
 
export function ProjectListPage() {
  const projects = useLoaderData() as Project[];
  const navigate = useNavigate();
  const revalidator = useRevalidator();
  const { showError } = useToast();
  const [demoOpen, setDemoOpen] = useState(false);
 
  const handleOpenFolder = useCallback(async () => {
    try {
      const handle = await showDirectoryPicker();
      const project = await addProject(handle.name, {
        provider: "filesystem",
        handle,
      });
      void navigate(`/projects/${project.slug}`);
    } catch (error) {
      if ((error as Error).name !== "AbortError") {
        showError({
          title: "Failed to open folder",
          description: (error as Error).message,
        });
      }
    }
  }, [navigate, showError]);
 
  const handleTryDemo = useCallback(
    async (name: string) => {
      try {
        const project = await addProject(name, {
          provider: "examples",
          name,
        });
        setDemoOpen(false);
        void navigate(`/projects/${project.slug}`);
      } catch (error) {
        showError({
          title: "Failed to create demo project",
          description: (error as Error).message,
        });
      }
    },
    [navigate, showError],
  );
 
  const handleRemove = useCallback(
    async (slug: string) => {
      try {
        await removeProject(slug);
        void revalidator.revalidate();
      } catch (error) {
        showError({
          title: "Failed to remove project",
          description: (error as Error).message,
        });
      }
    },
    [revalidator, showError],
  );
 
  return (
    <Box p="4">
      <Flex direction="column" gap="4" align="center">
        <Heading size="8">Beat Muser</Heading>
        <Flex gap="2">
          <Button onClick={handleOpenFolder}>Open Folder</Button>
          <Button variant="soft" onClick={() => setDemoOpen(true)}>
            Try Demo
          </Button>
        </Flex>
        <Flex direction="column" gap="2" width="100%" style={{ maxWidth: 600 }}>
          {projects.map((project) => (
            <Card
              key={project.slug}
              onClick={() => navigate(`/projects/${project.slug}`)}
              style={{ cursor: "pointer" }}
            >
              <Flex justify="between" align="center">
                <Flex direction="column">
                  <Text weight="bold">{project.displayName}</Text>
                  <Text size="1" color="gray">
                    Last opened {new Date(project.lastOpenedAt).toLocaleDateString()}
                  </Text>
                </Flex>
                <DropdownMenu.Root>
                  <DropdownMenu.Trigger>
                    <IconButton variant="ghost" size="1" onClick={(e) => e.stopPropagation()}>
                      ⋯
                    </IconButton>
                  </DropdownMenu.Trigger>
                  <DropdownMenu.Content>
                    <DropdownMenu.Item
                      color="red"
                      onClick={(e) => {
                        e.stopPropagation();
                        void handleRemove(project.slug);
                      }}
                    >
                      Remove
                    </DropdownMenu.Item>
                  </DropdownMenu.Content>
                </DropdownMenu.Root>
              </Flex>
            </Card>
          ))}
        </Flex>
      </Flex>
 
      <Dialog.Root open={demoOpen} onOpenChange={setDemoOpen}>
        <Dialog.Content maxWidth="450px">
          <Dialog.Title>Try a Demo Project</Dialog.Title>
          <Dialog.Description size="2" mb="4">
            Select a demo project to explore the app.
          </Dialog.Description>
          <Flex direction="column" gap="2">
            <Button variant="soft" onClick={() => handleTryDemo("demo1")}>
              Demo 1
            </Button>
            <Button variant="soft" onClick={() => handleTryDemo("demo2")}>
              Demo 2
            </Button>
          </Flex>
        </Dialog.Content>
      </Dialog.Root>
    </Box>
  );
}