TODOs: - [x] Do restart when checkout / restore if there is a DB - [x] List all branches (branch id, name, date) - [x] Allow checking out versions with no DB - [x] safeguard to never delete main branches - [x] create app hook for neon template - [x] weird UX with connector on configure panel - [x] tiny neon logo in connector - [x] deploy to vercel - [x] build forgot password page - [x] what about email setup - [x] lots of imgix errors - [x] edit file - db snapshot - [x] DYAD_DISABLE_DB_PUSH - [ ] update portal doc - [x] switch preview branch to be read-only endpoint - [x] disable supabase sys prompt if neon is enabled - [ ] https://payloadcms.com/docs/upload/storage-adapters - [x] need to use main branch... Phase 2? - [x] generate DB migrations
164 lines
5.2 KiB
TypeScript
164 lines
5.2 KiB
TypeScript
import React, { useState } from "react";
|
|
import { ArrowLeft } from "lucide-react";
|
|
import { IpcClient } from "@/ipc/ipc_client";
|
|
import { useSettings } from "@/hooks/useSettings";
|
|
import { CommunityCodeConsentDialog } from "./CommunityCodeConsentDialog";
|
|
import type { Template } from "@/shared/templates";
|
|
import { Button } from "./ui/button";
|
|
import { cn } from "@/lib/utils";
|
|
import { showWarning } from "@/lib/toast";
|
|
|
|
interface TemplateCardProps {
|
|
template: Template;
|
|
isSelected: boolean;
|
|
onSelect: (templateId: string) => void;
|
|
onCreateApp: () => void;
|
|
}
|
|
|
|
export const TemplateCard: React.FC<TemplateCardProps> = ({
|
|
template,
|
|
isSelected,
|
|
onSelect,
|
|
onCreateApp,
|
|
}) => {
|
|
const { settings, updateSettings } = useSettings();
|
|
const [showConsentDialog, setShowConsentDialog] = useState(false);
|
|
|
|
const handleCardClick = () => {
|
|
// If it's a community template and user hasn't accepted community code yet, show dialog
|
|
if (!template.isOfficial && !settings?.acceptedCommunityCode) {
|
|
setShowConsentDialog(true);
|
|
return;
|
|
}
|
|
|
|
if (template.requiresNeon && !settings?.neon?.accessToken) {
|
|
showWarning("Please connect your Neon account to use this template.");
|
|
return;
|
|
}
|
|
|
|
// Otherwise, proceed with selection
|
|
onSelect(template.id);
|
|
};
|
|
|
|
const handleConsentAccept = () => {
|
|
// Update settings to accept community code
|
|
updateSettings({ acceptedCommunityCode: true });
|
|
|
|
// Select the template
|
|
onSelect(template.id);
|
|
|
|
// Close dialog
|
|
setShowConsentDialog(false);
|
|
};
|
|
|
|
const handleConsentCancel = () => {
|
|
// Just close dialog, don't update settings or select template
|
|
setShowConsentDialog(false);
|
|
};
|
|
|
|
const handleGithubClick = (e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
if (template.githubUrl) {
|
|
IpcClient.getInstance().openExternalUrl(template.githubUrl);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
onClick={handleCardClick}
|
|
className={`
|
|
bg-white dark:bg-gray-800 rounded-xl shadow-sm overflow-hidden
|
|
transform transition-all duration-300 ease-in-out
|
|
cursor-pointer group relative
|
|
${
|
|
isSelected
|
|
? "ring-2 ring-blue-500 dark:ring-blue-400 shadow-xl"
|
|
: "hover:shadow-lg hover:-translate-y-1"
|
|
}
|
|
`}
|
|
>
|
|
<div className="relative">
|
|
<img
|
|
src={template.imageUrl}
|
|
alt={template.title}
|
|
className={`w-full h-52 object-cover transition-opacity duration-300 group-hover:opacity-80 ${
|
|
isSelected ? "opacity-75" : ""
|
|
}`}
|
|
/>
|
|
{isSelected && (
|
|
<span className="absolute top-3 right-3 bg-blue-600 text-white text-xs font-bold px-3 py-1.5 rounded-md shadow-lg">
|
|
Selected
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="p-4">
|
|
<div className="flex justify-between items-center mb-1.5">
|
|
<h2
|
|
className={`text-lg font-semibold ${
|
|
isSelected
|
|
? "text-blue-600 dark:text-blue-400"
|
|
: "text-gray-900 dark:text-white"
|
|
}`}
|
|
>
|
|
{template.title}
|
|
</h2>
|
|
{template.isOfficial && !template.isExperimental && (
|
|
<span
|
|
className={`text-xs font-semibold px-2 py-0.5 rounded-full ${
|
|
isSelected
|
|
? "bg-blue-100 text-blue-700 dark:bg-blue-600 dark:text-blue-100"
|
|
: "bg-green-100 text-green-800 dark:bg-green-700 dark:text-green-200"
|
|
}`}
|
|
>
|
|
Official
|
|
</span>
|
|
)}
|
|
{template.isExperimental && (
|
|
<span className="text-xs font-semibold px-2 py-0.5 rounded-full bg-yellow-100 text-yellow-800 dark:bg-yellow-700 dark:text-yellow-200">
|
|
Experimental
|
|
</span>
|
|
)}
|
|
</div>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400 mb-3 h-10 overflow-y-auto">
|
|
{template.description}
|
|
</p>
|
|
{template.githubUrl && (
|
|
<a
|
|
className={`inline-flex items-center text-sm font-medium transition-colors duration-200 ${
|
|
isSelected
|
|
? "text-blue-500 hover:text-blue-700 dark:text-blue-300 dark:hover:text-blue-200"
|
|
: "text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
|
|
}`}
|
|
onClick={handleGithubClick}
|
|
>
|
|
View on GitHub{" "}
|
|
<ArrowLeft className="w-4 h-4 ml-1 transform rotate-180" />
|
|
</a>
|
|
)}
|
|
|
|
<Button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onCreateApp();
|
|
}}
|
|
size="sm"
|
|
className={cn(
|
|
"w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold mt-2",
|
|
settings?.selectedTemplateId !== template.id && "invisible",
|
|
)}
|
|
>
|
|
Create App
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<CommunityCodeConsentDialog
|
|
isOpen={showConsentDialog}
|
|
onAccept={handleConsentAccept}
|
|
onCancel={handleConsentCancel}
|
|
/>
|
|
</>
|
|
);
|
|
};
|