优化项目启动器的创建功能
This commit is contained in:
@@ -1,19 +1,37 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useState, useEffect, useCallback } from "react"
|
||||
import { useTranslations } from "next-intl"
|
||||
import { Loader2, FolderOpen } from "lucide-react"
|
||||
import {
|
||||
Loader2,
|
||||
FolderOpen,
|
||||
ChevronsUpDown,
|
||||
CircleCheck,
|
||||
CircleX,
|
||||
} from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
Tabs,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
TabsContent,
|
||||
} from "@/components/ui/tabs"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldLabel,
|
||||
FieldTitle,
|
||||
} from "@/components/ui/field"
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -22,9 +40,17 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { openFileDialog } from "@/lib/platform"
|
||||
import { createShadcnProject, openFolderWindow } from "@/lib/api"
|
||||
import {
|
||||
createShadcnProject,
|
||||
openFolderWindow,
|
||||
detectPackageManager,
|
||||
} from "@/lib/api"
|
||||
import { toErrorMessage } from "@/lib/app-error"
|
||||
import { FRAMEWORK_OPTIONS, PACKAGE_MANAGER_OPTIONS } from "./constants"
|
||||
import {
|
||||
BASE_OPTIONS,
|
||||
FRAMEWORK_OPTIONS,
|
||||
PACKAGE_MANAGER_OPTIONS,
|
||||
} from "./constants"
|
||||
|
||||
interface CreateProjectDialogProps {
|
||||
open: boolean
|
||||
@@ -42,9 +68,38 @@ export function CreateProjectDialog({
|
||||
const [framework, setFramework] = useState("next")
|
||||
const [packageManager, setPackageManager] = useState("pnpm")
|
||||
const [saveDirectory, setSaveDirectory] = useState("")
|
||||
const [base, setBase] = useState("radix")
|
||||
const [rtl, setRtl] = useState(false)
|
||||
const [advancedOpen, setAdvancedOpen] = useState(false)
|
||||
const [creating, setCreating] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const [pmVersion, setPmVersion] = useState<string | null>(null)
|
||||
const [pmInstalled, setPmInstalled] = useState<boolean | null>(null)
|
||||
const [pmChecking, setPmChecking] = useState(false)
|
||||
|
||||
const checkPackageManager = useCallback(async (name: string) => {
|
||||
setPmChecking(true)
|
||||
setPmInstalled(null)
|
||||
setPmVersion(null)
|
||||
try {
|
||||
const info = await detectPackageManager(name)
|
||||
setPmInstalled(info.installed)
|
||||
setPmVersion(info.version ?? null)
|
||||
} catch {
|
||||
setPmInstalled(false)
|
||||
setPmVersion(null)
|
||||
} finally {
|
||||
setPmChecking(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
checkPackageManager(packageManager)
|
||||
}
|
||||
}, [open, packageManager, checkPackageManager])
|
||||
|
||||
const handleBrowse = async () => {
|
||||
const result = await openFileDialog({ directory: true, multiple: false })
|
||||
if (!result) return
|
||||
@@ -81,11 +136,18 @@ export function CreateProjectDialog({
|
||||
setFramework("next")
|
||||
setPackageManager("pnpm")
|
||||
setSaveDirectory("")
|
||||
setBase("radix")
|
||||
setRtl(false)
|
||||
setAdvancedOpen(false)
|
||||
setError(null)
|
||||
setPmVersion(null)
|
||||
setPmInstalled(null)
|
||||
}
|
||||
|
||||
const canCreate =
|
||||
projectName.trim().length > 0 && saveDirectory.trim().length > 0
|
||||
projectName.trim().length > 0 &&
|
||||
saveDirectory.trim().length > 0 &&
|
||||
pmInstalled === true
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@@ -111,46 +173,6 @@ export function CreateProjectDialog({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label>{t("createDialog.frameworkTemplate")}</Label>
|
||||
<Select
|
||||
value={framework}
|
||||
onValueChange={setFramework}
|
||||
disabled={creating}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{FRAMEWORK_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label>{t("createDialog.packageManager")}</Label>
|
||||
<Select
|
||||
value={packageManager}
|
||||
onValueChange={setPackageManager}
|
||||
disabled={creating}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PACKAGE_MANAGER_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label>{t("createDialog.saveDirectory")}</Label>
|
||||
<div className="flex gap-2">
|
||||
@@ -173,6 +195,135 @@ export function CreateProjectDialog({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label>{t("createDialog.packageManager")}</Label>
|
||||
<Tabs
|
||||
value={packageManager}
|
||||
onValueChange={setPackageManager}
|
||||
className="gap-0"
|
||||
>
|
||||
<TabsList className="w-full">
|
||||
{PACKAGE_MANAGER_OPTIONS.map((opt) => (
|
||||
<TabsTrigger
|
||||
key={opt.value}
|
||||
value={opt.value}
|
||||
className="flex-1"
|
||||
disabled={creating}
|
||||
>
|
||||
{opt.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{PACKAGE_MANAGER_OPTIONS.map((opt) => (
|
||||
<TabsContent key={opt.value} value={opt.value}>
|
||||
<div className="flex h-8 items-center gap-1.5 text-sm">
|
||||
{pmChecking ? (
|
||||
<>
|
||||
<Loader2 className="size-3.5 animate-spin text-muted-foreground" />
|
||||
<span className="text-muted-foreground">
|
||||
{t("createDialog.pmChecking")}
|
||||
</span>
|
||||
</>
|
||||
) : pmInstalled ? (
|
||||
<>
|
||||
<CircleCheck className="size-3.5 text-emerald-500" />
|
||||
<span className="text-muted-foreground">
|
||||
{opt.label} v{pmVersion}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CircleX className="size-3.5 text-destructive" />
|
||||
<span className="text-muted-foreground">
|
||||
{t("createDialog.pmNotInstalled")}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-auto gap-1 px-0 text-xs text-muted-foreground"
|
||||
disabled={creating}
|
||||
>
|
||||
<ChevronsUpDown className="size-3.5" />
|
||||
{t("createDialog.advancedOptions")}
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="space-y-4 pt-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label>{t("createDialog.frameworkTemplate")}</Label>
|
||||
<RadioGroup
|
||||
value={framework}
|
||||
onValueChange={setFramework}
|
||||
disabled={creating}
|
||||
className="grid grid-cols-2 gap-2"
|
||||
>
|
||||
{FRAMEWORK_OPTIONS.map((opt) => (
|
||||
<FieldLabel key={opt.value} htmlFor={`fw-${opt.value}`}>
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>{opt.label}</FieldTitle>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value={opt.value}
|
||||
id={`fw-${opt.value}`}
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label>{t("createDialog.base")}</Label>
|
||||
<RadioGroup
|
||||
value={base}
|
||||
onValueChange={setBase}
|
||||
disabled={creating}
|
||||
className="grid grid-cols-2 gap-2"
|
||||
>
|
||||
{BASE_OPTIONS.map((opt) => (
|
||||
<FieldLabel key={opt.value} htmlFor={`base-${opt.value}`}>
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>{opt.label}</FieldTitle>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value={opt.value}
|
||||
id={`base-${opt.value}`}
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<label className="flex cursor-pointer items-center gap-3 rounded-lg border p-3">
|
||||
<Switch
|
||||
checked={rtl}
|
||||
onCheckedChange={setRtl}
|
||||
disabled={creating}
|
||||
/>
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-sm font-medium">
|
||||
{t("createDialog.enableRtl")}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t("createDialog.enableRtlDescription")}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-lg border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
||||
{error}
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
} from "@/components/ui/select"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import {
|
||||
BASE_OPTIONS,
|
||||
STYLE_OPTIONS,
|
||||
BASE_COLOR_OPTIONS,
|
||||
THEME_OPTIONS,
|
||||
@@ -35,7 +34,6 @@ interface ShadcnConfigPanelProps {
|
||||
}
|
||||
|
||||
type ConfigI18nKey =
|
||||
| "config.base"
|
||||
| "config.style"
|
||||
| "config.baseColor"
|
||||
| "config.theme"
|
||||
@@ -53,7 +51,6 @@ const CONFIG_FIELDS: {
|
||||
i18nKey: ConfigI18nKey
|
||||
options: { value: string; label: string }[]
|
||||
}[] = [
|
||||
{ key: "base", i18nKey: "config.base", options: BASE_OPTIONS },
|
||||
{ key: "style", i18nKey: "config.style", options: STYLE_OPTIONS },
|
||||
{ key: "baseColor", i18nKey: "config.baseColor", options: BASE_COLOR_OPTIONS },
|
||||
{ key: "theme", i18nKey: "config.theme", options: THEME_OPTIONS },
|
||||
|
||||
Reference in New Issue
Block a user