Polish app details & supabase connector UX

This commit is contained in:
Will Chen
2025-04-23 12:21:40 -07:00
parent 9828cb3db9
commit 1d0176d1e9
9 changed files with 234 additions and 116 deletions

View File

@@ -173,11 +173,11 @@ export function GitHubConnector({ appId, folderName }: GitHubConnectorProps) {
if (!settings?.githubAccessToken) {
return (
<div className="mt-4 w-full">
<div className="mt-1 w-full">
{" "}
<Button
onClick={handleConnectToGithub}
className="cursor-pointer w-full py-6 flex justify-center items-center gap-2 text-lg"
className="cursor-pointer w-full py-5 flex justify-center items-center gap-2"
size="lg"
variant="outline"
disabled={isConnectingToGithub || !appId} // Also disable if appId is null

View File

@@ -25,13 +25,19 @@ import { Skeleton } from "@/components/ui/skeleton";
import { useLoadApp } from "@/hooks/useLoadApp";
import { useDeepLink } from "@/contexts/DeepLinkContext";
const OAUTH_CLIENT_ID = "bf747de7-60bb-48a2-9015-6494e0b04983";
import supabaseLogoLight from "../../assets/supabase/supabase-logo-wordmark--light.svg";
import supabaseLogoDark from "../../assets/supabase/supabase-logo-wordmark--dark.svg";
import connectSupabaseDark from "../../assets/supabase/connect-supabase-dark.svg";
import connectSupabaseLight from "../../assets/supabase/connect-supabase-light.svg";
import { ExternalLink } from "lucide-react";
import { useTheme } from "@/contexts/ThemeContext";
export function SupabaseConnector({ appId }: { appId: number }) {
const [isConnecting, setIsConnecting] = useState(false);
const { settings, refreshSettings } = useSettings();
const { app, refreshApp } = useLoadApp(appId);
const { lastDeepLink } = useDeepLink();
const { isDarkMode } = useTheme();
useEffect(() => {
const handleDeepLink = async () => {
if (lastDeepLink?.type === "supabase-oauth-return") {
@@ -58,27 +64,6 @@ export function SupabaseConnector({ appId }: { appId: number }) {
}
}, [settings?.supabase?.accessToken, loadProjects]);
const handleConnect = async () => {
try {
setIsConnecting(true);
// TODO: replace this with deployed URL
const result = await IpcClient.getInstance().openExternalUrl(
"https://supabase-oauth.dyad.sh/api/connect-supabase/login"
);
if (!result.success) {
throw new Error(result.error || "Failed to open auth URL");
}
toast.success("Successfully connected to Supabase");
} catch (error) {
console.error("Failed to connect to Supabase:", error);
toast.error("Failed to connect to Supabase");
} finally {
setIsConnecting(false);
}
};
const handleProjectSelect = async (projectId: string) => {
try {
await setAppProject(projectId, appId);
@@ -103,9 +88,31 @@ export function SupabaseConnector({ appId }: { appId: number }) {
if (settings?.supabase?.accessToken) {
if (app?.supabaseProjectName) {
return (
<Card>
<Card className="mt-1">
<CardHeader>
<CardTitle>Supabase Project</CardTitle>
<CardTitle className="flex items-center justify-between">
Supabase Project{" "}
<Button
variant="outline"
onClick={() => {
IpcClient.getInstance().openExternalUrl(
`https://supabase.com/dashboard/project/${app.supabaseProjectId}`
);
}}
className="ml-2 px-2 py-1"
style={{ display: "inline-flex", alignItems: "center" }}
asChild
>
<div className="flex items-center gap-2">
<img
src={isDarkMode ? supabaseLogoDark : supabaseLogoLight}
alt="Supabase Logo"
style={{ height: 20, width: "auto", marginRight: 4 }}
/>
<ExternalLink className="h-4 w-4" />
</div>
</Button>
</CardTitle>
<CardDescription>
This app is connected to project: {app.supabaseProjectName}
</CardDescription>
@@ -119,7 +126,7 @@ export function SupabaseConnector({ appId }: { appId: number }) {
);
}
return (
<Card>
<Card className="mt-1">
<CardHeader>
<CardTitle>Supabase Projects</CardTitle>
<CardDescription>
@@ -188,15 +195,20 @@ export function SupabaseConnector({ appId }: { appId: number }) {
return (
<div className="flex flex-col space-y-4 p-4 border rounded-md">
<h2 className="text-lg font-semibold">Supabase Integration</h2>
<Button
onClick={handleConnect}
disabled={isConnecting}
className="w-full"
>
{isConnecting ? "Connecting..." : "Connect to Supabase"}
</Button>
<div className="flex flex-col md:flex-row items-center justify-between">
<h2 className="text-lg font-medium">Integrations</h2>
<img
onClick={() => {
IpcClient.getInstance().openExternalUrl(
"https://supabase-oauth.dyad.sh/api/connect-supabase/login"
);
}}
src={isDarkMode ? connectSupabaseDark : connectSupabaseLight}
alt="Connect to Supabase"
className="w-full h-10 min-h-8 min-w-20 cursor-pointer"
// className="h-10"
/>
</div>
</div>
);
}

View File

@@ -17,28 +17,7 @@ export const CodeHighlight = memo(
const language = className?.match(/language-(\w+)/)?.[1];
const isInline = node ? isInlineCode(node) : false;
// Get the current theme setting
const { theme } = useTheme();
// State to track if dark mode is active
const [isDarkMode, setIsDarkMode] = React.useState(false);
// Determine if dark mode is active when component mounts or theme changes
useEffect(() => {
const darkModeQuery = window.matchMedia("(prefers-color-scheme: dark)");
const updateTheme = () => {
setIsDarkMode(
theme === "dark" || (theme === "system" && darkModeQuery.matches)
);
};
updateTheme();
darkModeQuery.addEventListener("change", updateTheme);
return () => {
darkModeQuery.removeEventListener("change", updateTheme);
};
}, [theme]);
const { isDarkMode } = useTheme();
// Cache for the highlighted code
const highlightedCodeCache = useRef<ReactNode | null>(null);

View File

@@ -53,5 +53,24 @@ export function useTheme() {
if (context === undefined) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
const [isDarkMode, setIsDarkMode] = useState(false);
const { theme, setTheme } = context;
// Determine if dark mode is active when component mounts or theme changes
useEffect(() => {
const darkModeQuery = window.matchMedia("(prefers-color-scheme: dark)");
const updateTheme = () => {
setIsDarkMode(
theme === "dark" || (theme === "system" && darkModeQuery.matches)
);
};
updateTheme();
darkModeQuery.addEventListener("change", updateTheme);
return () => {
darkModeQuery.removeEventListener("change", updateTheme);
};
}, [theme]);
return { theme, isDarkMode, setTheme };
}

View File

@@ -147,59 +147,66 @@ export default function AppDetailsPage() {
onClick={() => router.history.back()}
variant="outline"
size="sm"
className="absolute top-4 left-4 flex items-center gap-2 bg-(--background-lightest) py-5"
className="absolute top-4 left-4 flex items-center gap-1 bg-(--background-lightest) py-5"
>
<ArrowLeft className="h-4 w-4" />
Go Back
<ArrowLeft className="h-3 w-4" />
Back
</Button>
<div className="flex flex-col items-center justify-center h-full">
<h2 className="text-2xl font-bold mb-4">App not found</h2>
<h2 className="text-xl font-bold">App not found</h2>
</div>
</div>
);
}
return (
<div className="relative min-h-screen p-8 w-full">
<div className="relative min-h-screen p-4 w-full">
<Button
onClick={() => router.history.back()}
variant="outline"
size="sm"
className="absolute top-4 left-4 flex items-center gap-2 bg-(--background-lightest) py-5"
className="absolute top-4 left-4 flex items-center gap-1 bg-(--background-lightest) py-2"
>
<ArrowLeft className="h-4 w-4" />
Go Back
<ArrowLeft className="h-3 w-4" />
Back
</Button>
<div className="w-full max-w-2xl mx-auto mt-16 p-8 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-md relative">
<div className="flex items-center mb-6">
<h2 className="text-3xl font-bold">{selectedApp.name}</h2>
<div className="w-full max-w-2xl mx-auto mt-10 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm relative">
<div className="flex items-center mb-3">
<h2 className="text-2xl font-bold">{selectedApp.name}</h2>
<Button
variant="ghost"
size="sm"
className="ml-2 p-1 h-auto"
className="ml-1 p-0.5 h-auto"
onClick={handleOpenRenameDialog}
>
<Pencil className="h-4 w-4" />
<Pencil className="h-3.5 w-3.5" />
</Button>
</div>
{/* Overflow Menu in top right */}
<div className="absolute top-4 right-4">
<div className="absolute top-2 right-2">
<Popover>
<PopoverTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<Button variant="ghost" size="sm" className="h-7 w-7 p-0">
<MoreVertical className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-48" align="end">
<div className="flex flex-col space-y-1">
<Button onClick={handleOpenRenameFolderDialog} variant="ghost">
<PopoverContent className="w-40 p-2" align="end">
<div className="flex flex-col space-y-0.5">
<Button
onClick={handleOpenRenameFolderDialog}
variant="ghost"
size="sm"
className="h-8 justify-start text-xs"
>
Rename folder
</Button>
<Button
onClick={() => setIsDeleteDialogOpen(true)}
variant="ghost"
size="sm"
className="h-8 justify-start text-xs"
>
Delete
</Button>
@@ -208,38 +215,38 @@ export default function AppDetailsPage() {
</Popover>
</div>
<div className="grid grid-cols-2 gap-6 text-base mb-8">
<div className="grid grid-cols-2 gap-3 text-sm mb-4">
<div>
<span className="block text-gray-500 dark:text-gray-400 mb-1 text-base">
<span className="block text-gray-500 dark:text-gray-400 mb-0.5 text-xs">
Created
</span>
<span>{new Date().toLocaleString()}</span>
</div>
<div>
<span className="block text-gray-500 dark:text-gray-400 mb-1 text-base">
<span className="block text-gray-500 dark:text-gray-400 mb-0.5 text-xs">
Last Updated
</span>
<span>{new Date().toLocaleString()}</span>
</div>
<div className="col-span-2">
<span className="block text-gray-500 dark:text-gray-400 mb-1 text-base">
<span className="block text-gray-500 dark:text-gray-400 mb-0.5 text-xs">
Path
</span>
<span>
<span className="text-sm break-all">
{appBasePath.replace("$APP_BASE_PATH", selectedApp.path)}
</span>
</div>
</div>
<div className="mt-8 flex flex-col gap-4">
<div className="mt-4 flex flex-col gap-2">
<Button
onClick={() =>
appId && navigate({ to: "/chat", search: { id: appId } })
}
className="cursor-pointer w-full py-6 flex justify-center items-center gap-2 text-lg"
className="cursor-pointer w-full py-5 flex justify-center items-center gap-2"
size="lg"
>
Open in Chat
<MessageCircle className="h-5 w-5" />
<MessageCircle className="h-4 w-4" />
</Button>
<GitHubConnector appId={appId} folderName={selectedApp.path} />
{appId && settings?.experiments?.enableSupabaseIntegration && (
@@ -249,22 +256,23 @@ export default function AppDetailsPage() {
{/* Rename Dialog */}
<Dialog open={isRenameDialogOpen} onOpenChange={setIsRenameDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogContent className="max-w-sm p-4">
<DialogHeader className="pb-2">
<DialogTitle>Rename App</DialogTitle>
</DialogHeader>
<Input
value={newAppName}
onChange={(e) => setNewAppName(e.target.value)}
placeholder="Enter new app name"
className="my-4"
className="my-2"
autoFocus
/>
<DialogFooter>
<DialogFooter className="pt-2">
<Button
variant="outline"
onClick={() => setIsRenameDialogOpen(false)}
disabled={isRenaming}
size="sm"
>
Cancel
</Button>
@@ -274,6 +282,7 @@ export default function AppDetailsPage() {
setIsRenameConfirmDialogOpen(true);
}}
disabled={isRenaming || !newAppName.trim()}
size="sm"
>
Continue
</Button>
@@ -286,10 +295,10 @@ export default function AppDetailsPage() {
open={isRenameFolderDialogOpen}
onOpenChange={setIsRenameFolderDialogOpen}
>
<DialogContent>
<DialogHeader>
<DialogContent className="max-w-sm p-4">
<DialogHeader className="pb-2">
<DialogTitle>Rename app folder</DialogTitle>
<DialogDescription>
<DialogDescription className="text-xs">
This will change only the folder name, not the app name.
</DialogDescription>
</DialogHeader>
@@ -297,25 +306,27 @@ export default function AppDetailsPage() {
value={newFolderName}
onChange={(e) => setNewFolderName(e.target.value)}
placeholder="Enter new folder name"
className="my-4"
className="my-2"
autoFocus
/>
<DialogFooter>
<DialogFooter className="pt-2">
<Button
variant="outline"
onClick={() => setIsRenameFolderDialogOpen(false)}
disabled={isRenamingFolder}
size="sm"
>
Cancel
</Button>
<Button
onClick={handleRenameFolderOnly}
disabled={isRenamingFolder || !newFolderName.trim()}
size="sm"
>
{isRenamingFolder ? (
<>
<svg
className="animate-spin h-4 w-4 mr-2"
className="animate-spin h-3 w-3 mr-1"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
@@ -349,28 +360,30 @@ export default function AppDetailsPage() {
open={isRenameConfirmDialogOpen}
onOpenChange={setIsRenameConfirmDialogOpen}
>
<DialogContent>
<DialogHeader>
<DialogTitle>
<DialogContent className="max-w-sm p-4">
<DialogHeader className="pb-2">
<DialogTitle className="text-base">
How would you like to rename "{selectedApp.name}"?
</DialogTitle>
<DialogDescription>Choose an option:</DialogDescription>
<DialogDescription className="text-xs">
Choose an option:
</DialogDescription>
</DialogHeader>
<div className="space-y-4 my-4">
<div className="space-y-2 my-2">
<Button
variant="outline"
className="w-full justify-start p-4 h-auto relative"
className="w-full justify-start p-2 h-auto relative text-sm"
onClick={() => handleRenameApp(true)}
disabled={isRenaming}
>
<div className="absolute top-2 right-2">
<span className="bg-blue-100 text-blue-800 text-xs font-medium px-2 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300">
<div className="absolute top-1 right-1">
<span className="bg-blue-100 text-blue-800 text-xs font-medium px-1.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300 text-[10px]">
Recommended
</span>
</div>
<div className="text-left">
<p className="font-medium">Rename app and folder</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
<p className="font-medium text-xs">Rename app and folder</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
Renames the folder to match the new app name.
</p>
</div>
@@ -378,23 +391,24 @@ export default function AppDetailsPage() {
<Button
variant="outline"
className="w-full justify-start p-4 h-auto"
className="w-full justify-start p-2 h-auto text-sm"
onClick={() => handleRenameApp(false)}
disabled={isRenaming}
>
<div className="text-left">
<p className="font-medium">Rename app only</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
<p className="font-medium text-xs">Rename app only</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
The folder name will remain the same.
</p>
</div>
</Button>
</div>
<DialogFooter>
<DialogFooter className="pt-2">
<Button
variant="outline"
onClick={() => setIsRenameConfirmDialogOpen(false)}
disabled={isRenaming}
size="sm"
>
Cancel
</Button>
@@ -404,19 +418,20 @@ export default function AppDetailsPage() {
{/* Delete Confirmation Dialog */}
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogContent className="max-w-sm p-4">
<DialogHeader className="pb-2">
<DialogTitle>Delete "{selectedApp.name}"?</DialogTitle>
<DialogDescription>
<DialogDescription className="text-xs">
This action is irreversible. All app files and chat history will
be permanently deleted.
</DialogDescription>
</DialogHeader>
<DialogFooter className="flex justify-end gap-3">
<DialogFooter className="flex justify-end gap-2 pt-2">
<Button
variant="outline"
onClick={() => setIsDeleteDialogOpen(false)}
disabled={isDeleting}
size="sm"
>
Cancel
</Button>
@@ -424,12 +439,13 @@ export default function AppDetailsPage() {
variant="destructive"
onClick={handleDeleteApp}
disabled={isDeleting}
className="flex items-center gap-2"
className="flex items-center gap-1"
size="sm"
>
{isDeleting ? (
<>
<svg
className="animate-spin h-4 w-4 text-white"
className="animate-spin h-3 w-3 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"