| 1 | import { |
| 2 | projectCloneDisplayName, |
| 3 | projectCloneProgressSummary, |
| 4 | type ProjectCloneSnapshot, |
| 5 | } from "@t3tools/contracts"; |
| 6 | import { Pressable, View } from "react-native"; |
| 7 | |
| 8 | import { AppActivityIndicator } from "./AppActivityIndicator"; |
| 9 | import { cn } from "../lib/cn"; |
| 10 | import { AppText as Text } from "./AppText"; |
| 11 | |
| 12 | /** |
| 13 | * Live state of the clone that backs a freshly added project, shown above |
| 14 | * the composer while the draft waits for its files. Running clones offer |
| 15 | * Cancel; failed or cancelled ones offer Retry and Remove project. |
| 16 | */ |
| 17 | export function ProjectCloneBanner(props: { |
| 18 | readonly clone: ProjectCloneSnapshot; |
| 19 | readonly onCancel: () => void; |
| 20 | readonly onRetry: () => void; |
| 21 | readonly onRemove: () => void; |
| 22 | }) { |
| 23 | const { clone } = props; |
| 24 | const name = projectCloneDisplayName(clone); |
| 25 | if (clone.phase === "running") { |
| 26 | return ( |
| 27 | <View className="flex-row items-center gap-3 rounded-2xl border border-border bg-card px-3.5 py-3"> |
| 28 | <AppActivityIndicator size="small" /> |
| 29 | <View className="min-w-0 flex-1"> |
| 30 | <Text className="font-t3-medium text-sm" numberOfLines={1}> |
| 31 | Cloning {name} |
| 32 | </Text> |
| 33 | <Text className="text-xs text-foreground-muted" numberOfLines={1}> |
| 34 | {projectCloneProgressSummary(clone)} |
| 35 | </Text> |
| 36 | </View> |
| 37 | <BannerAction label="Cancel" onPress={props.onCancel} /> |
| 38 | </View> |
| 39 | ); |
| 40 | } |
| 41 | const cancelled = clone.phase === "cancelled"; |
| 42 | return ( |
| 43 | <View |
| 44 | className={cn( |
| 45 | "rounded-2xl border px-3.5 py-3", |
| 46 | cancelled ? "border-warning-border bg-warning" : "border-danger-border bg-danger", |
| 47 | )} |
| 48 | > |
| 49 | <Text |
| 50 | className={cn( |
| 51 | "font-t3-medium text-sm", |
| 52 | cancelled ? "text-warning-foreground" : "text-danger-foreground", |
| 53 | )} |
| 54 | numberOfLines={1} |
| 55 | > |
| 56 | {cancelled ? `Cancelled cloning ${name}` : `Failed to clone ${name}`} |
| 57 | </Text> |
| 58 | {clone.error ? ( |
| 59 | <Text className="mt-0.5 text-xs text-danger-foreground" numberOfLines={3}> |
| 60 | {clone.error} |
| 61 | </Text> |
| 62 | ) : null} |
| 63 | <View className="mt-2 flex-row justify-end gap-2"> |
| 64 | <BannerAction label="Remove project" onPress={props.onRemove} /> |
| 65 | <BannerAction label="Retry" onPress={props.onRetry} /> |
| 66 | </View> |
| 67 | </View> |
| 68 | ); |
| 69 | } |
| 70 | |
| 71 | function BannerAction(props: { readonly label: string; readonly onPress: () => void }) { |
| 72 | return ( |
| 73 | <Pressable |
| 74 | accessibilityRole="button" |
| 75 | accessibilityLabel={props.label} |
| 76 | className="rounded-full border border-border bg-card px-3 py-1.5" |
| 77 | onPress={props.onPress} |
| 78 | > |
| 79 | <Text className="font-t3-medium text-xs">{props.label}</Text> |
| 80 | </Pressable> |
| 81 | ); |
| 82 | } |