diff --git a/.nvmrc b/.nvmrc index 68c98aa..12419e9 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v18.18.2 \ No newline at end of file +v20.18.1 \ No newline at end of file diff --git a/apps/web/.env.example b/apps/web/.env.example index 600bf7d..0f4d639 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -1,4 +1,6 @@ OPENAI_API_KEY= -UPSTASH_REDIS_REST_URL= -UPSTASH_REDIS_REST_TOKEN= GOOGLE_GENERATIVE_AI_API_KEY= +DEFAULT_PROVIDER=openai + +UPSTASH_REDIS_REST_URL= +UPSTASH_REDIS_REST_TOKEN= \ No newline at end of file diff --git a/apps/web/actions.ts b/apps/web/actions.ts deleted file mode 100644 index 8ef91d3..0000000 --- a/apps/web/actions.ts +++ /dev/null @@ -1,28 +0,0 @@ -'use server' - -import { Redis } from '@upstash/redis' -import { revalidatePath } from 'next/cache' -import { cookies } from 'next/headers' -import { TTL_SECONDS } from '@/constants' - -export const saveGeneration = async (data: { sqlSchema: string; cmdCode: string }) => { - const { cmdCode, sqlSchema } = data - const redis = Redis.fromEnv() - const res = await redis.set(cmdCode, sqlSchema) - await redis.expire(cmdCode, TTL_SECONDS) - - return res -} - -export const setApiKey = (prevState: any, formData: FormData) => { - const apiKey = formData.get('key') as string - cookies().set('api-key', apiKey, { - secure: true - }) - revalidatePath('/') - return { msg: 'Key Saved Successfully' } -} - -export const getApiKey = async () => { - return cookies().get('api-key')?.value -} diff --git a/apps/web/app/api/code-generation/route.ts b/apps/web/app/api/ai-generation/route.ts similarity index 54% rename from apps/web/app/api/code-generation/route.ts rename to apps/web/app/api/ai-generation/route.ts index f0c6194..f0fcd6f 100644 --- a/apps/web/app/api/code-generation/route.ts +++ b/apps/web/app/api/ai-generation/route.ts @@ -1,24 +1,15 @@ import { NextResponse } from 'next/server' -import { cookies } from 'next/headers' -import { streamText } from 'ai' -import { createOpenAI } from '@ai-sdk/openai' -import { PROMPT } from '@/prompt' +import { generateObject } from 'ai' +import { openai } from '@ai-sdk/openai' +import { uptash } from '@/utils/rate-limit' +import { headers } from 'next/headers' -export const runtime = 'edge' +import { DB_SCHEMA, prompts } from '@/utils/ai' -export async function POST(req: Request) { - let customApiKey = cookies().get('api-key')?.value - - if (process.env.NODE_ENV === 'production' && !customApiKey) { - return NextResponse.json( - { - data: undefined, - message: 'Missing API KEY – make sure to set it.' - }, - { status: 400 } - ) - } +const ratelimit = + process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN ? uptash : false +export async function POST(req: Request) { if ( process.env.NODE_ENV === 'development' && (!process.env.OPENAI_API_KEY || process.env.OPENAI_API_KEY === '') @@ -32,27 +23,33 @@ export async function POST(req: Request) { ) } - if (process.env.NODE_ENV === 'development') { - customApiKey = process.env.OPENAI_API_KEY - } + if (process.env.NODE_ENV === 'production') { + if (ratelimit) { + const ip = (await headers()).get('x-forwarded-for') ?? 'local' - const openai = createOpenAI({ - apiKey: customApiKey, - compatibility: 'strict' - }) + const { success } = await ratelimit.limit(ip) + if (!success) { + return NextResponse.json( + { message: 'You have reached your request limit for the day.' }, + { status: 429 } + ) + } + } + } - const { prompt: base64 } = await req.json() + const { prompt: base64, databaseFormat } = await req.json() try { - const result = await streamText({ - model: openai('gpt-4o'), + const result = await generateObject({ + model: openai('gpt-4.1-mini'), + schema: DB_SCHEMA, messages: [ { role: 'user', content: [ { type: 'text', - text: PROMPT + text: prompts[databaseFormat] as string }, { type: 'image', @@ -61,22 +58,22 @@ export async function POST(req: Request) { ] } ], - maxTokens: 4096, temperature: 0.2 }) - return result.toAIStreamResponse() + return NextResponse.json({ + data: result.object.results + }) } catch (error) { - // console.log(Object.keys(error)) // @ts-ignore const statusCode = error?.lastError?.statusCode ?? error.statusCode let errorMessage = 'An error has ocurred with API Completions. Please try again.' if (statusCode === 401) { errorMessage = 'The provided API Key is invalid. Please enter a valid API Key.' - } else if (statusCode === 429) { + } /*else if (statusCode === 429) { errorMessage = 'You exceeded your current quota, please check your plan and billing details.' - } + }*/ return NextResponse.json( { diff --git a/apps/web/app/api/check-connection/route.ts b/apps/web/app/api/check-connection/route.ts deleted file mode 100644 index 2e19916..0000000 --- a/apps/web/app/api/check-connection/route.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { NextResponse } from 'next/server' -import { drizzle } from 'drizzle-orm/postgres-js' -import postgres from 'postgres' -import { sql } from 'drizzle-orm' - -type ResponseJson = { - url: string -} - -export async function POST(req: Request) { - const { url } = (await req.json()) as ResponseJson - - if (url === '') { - return NextResponse.json( - { - error: - "We couldn't find a connection URL. Please try again with the correct connection URL." - }, - { status: 400 } - ) - } - // Disable prefetch as it is not supported for "Transaction" pool mode - const client = postgres(url, { prepare: false }) - const db = drizzle(client) - - // Check if connection is successful - try { - await db.execute(sql`SELECT NOW()`) - } catch (error) { - // @ts-ignore - let message = error.code - if (message === 'SASL_SIGNATURE_MISMATCH') { - message = 'Database password is missing.' - } else if (message === 'ENOTFOUND') { - message = - 'Your connection URL is invalid. Please double-check it and make the necessary corrections.' - } - return NextResponse.json({ error: message }, { status: 500 }) - } - - return NextResponse.json({ message: 'Connection stablished succesfully' }) -} diff --git a/apps/web/app/api/deploy/route.ts b/apps/web/app/api/deploy/route.ts index 32bdfca..814ff9d 100644 --- a/apps/web/app/api/deploy/route.ts +++ b/apps/web/app/api/deploy/route.ts @@ -1,7 +1,5 @@ import { NextResponse } from 'next/server' -import { drizzle } from 'drizzle-orm/postgres-js' -import postgres from 'postgres' -import { sql } from 'drizzle-orm' +import { deploySchema } from '@/utils/database' type ResponseJson = { url: string @@ -20,26 +18,11 @@ export async function POST(req: Request) { { status: 400 } ) } - // Disable prefetch as it is not supported for "Transaction" pool mode - const client = postgres(url, { prepare: false }) - const db = drizzle(client) - // Check if connection is successful - try { - await db.execute(sql`SELECT NOW()`) - } catch (error) { - // @ts-ignore - let message = error.code - if (message === 'SASL_SIGNATURE_MISMATCH') { - message = 'Database password is missing.' - } else if (message === 'ENOTFOUND') { - message = - 'Your connection URL is invalid. Please double-check it and make the necessary corrections.' - } - return NextResponse.json({ error: message }, { status: 500 }) - } - // Execute the migration - await db.execute(sql.raw(sqlSchema)) + const response = await deploySchema(url, sqlSchema) + if (!response.success) { + return NextResponse.json({ error: response.message }, { status: 500 }) + } return NextResponse.json({ message: 'Database Schema deployed successfully' diff --git a/apps/web/app/api/gemini-generation/route.ts b/apps/web/app/api/gemini-generation/route.ts index 05dea26..0bda631 100644 --- a/apps/web/app/api/gemini-generation/route.ts +++ b/apps/web/app/api/gemini-generation/route.ts @@ -1,9 +1,13 @@ import { NextResponse } from 'next/server' -import { StreamingTextResponse, streamText } from 'ai' +import { generateObject } from 'ai' import { google } from '@ai-sdk/google' -import { PROMPT } from '@/prompt' +import { uptash } from '@/utils/rate-limit' +import { headers } from 'next/headers' -export const runtime = 'edge' +import { DB_SCHEMA, prompts } from '@/utils/ai' + +const ratelimit = + process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN ? uptash : false export async function POST(req: Request) { if (process.env.NODE_ENV === 'development' && !process.env.GOOGLE_GENERATIVE_AI_API_KEY) { @@ -16,18 +20,33 @@ export async function POST(req: Request) { ) } - const { prompt: base64 } = await req.json() + if (process.env.NODE_ENV === 'production') { + if (ratelimit) { + const ip = (await headers()).get('x-forwarded-for') ?? 'local' + + const { success } = await ratelimit.limit(ip) + if (!success) { + return NextResponse.json( + { message: 'You have reached your request limit for the day.' }, + { status: 429 } + ) + } + } + } + + const { prompt: base64, databaseFormat } = await req.json() try { - const result = await streamText({ - model: google('models/gemini-1.5-flash-latest'), + const result = await generateObject({ + model: google('gemini-2.0-flash-001'), + schema: DB_SCHEMA, messages: [ { role: 'user', content: [ { type: 'text', - text: PROMPT + text: prompts[databaseFormat] as string }, { type: 'image', @@ -36,11 +55,12 @@ export async function POST(req: Request) { ] } ], - maxTokens: 1024, temperature: 0.2 }) - return new StreamingTextResponse(result.toAIStream()) + return NextResponse.json({ + data: result.object.results + }) } catch (error) { let errorMessage = 'An error has ocurred with API Completions. Please try again.' // @ts-ignore diff --git a/apps/web/app/api/get-generation/route.ts b/apps/web/app/api/get-generation/route.ts deleted file mode 100644 index 165ed70..0000000 --- a/apps/web/app/api/get-generation/route.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Redis } from '@upstash/redis' -import { NextResponse } from 'next/server' - -export const runtime = 'edge' - -const redis = Redis.fromEnv() - -export async function GET(req: Request) { - const { searchParams } = new URL(req.url) - const codeGeneration = searchParams.get('code') - if (!codeGeneration) { - return NextResponse.json( - { error: 'Code generation was not provided.' }, - { - status: 400 - } - ) - } - - try { - const code = await redis.get(codeGeneration) - - return NextResponse.json({ data: code }) - } catch (error) { - return NextResponse.json( - { error: 'An error has ocurred while fetching sql code.' }, - { - status: 500 - } - ) - } -} diff --git a/apps/web/app/favicon.ico b/apps/web/app/favicon.ico index 218c837..956a444 100644 Binary files a/apps/web/app/favicon.ico and b/apps/web/app/favicon.ico differ diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 20de83b..3b18147 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -10,20 +10,31 @@ import { Footer } from '@/components/footer' const inter = Inter({ subsets: ['latin'] }) -const title = 'vdbs - Transform Designs to Database Schemas with AI' +const title = 'Snap2SQL - Convert diagrams to SQL with AI' const description = - 'Manage database migrations with a single click, accessing AI-generated SQL code from a database diagram and a command line script for instant local execution.' + 'Snap2SQL lets you instantly convert database diagrams into clean SQL schemas using AI. Support for MySQL and PostgreSQL. Try your first scan free!' export const metadata: Metadata = { metadataBase: new URL(APP_URL), title, description, - keywords: ['vision ai', 'supabase', 'postgress', 'sql', 'migrations'], + keywords: [ + 'ERD to SQL', + 'diagram to SQL', + 'convert ERD', + 'SQL schema generator', + 'AI SQL builder', + 'database diagram OCR', + 'MySQL generator', + 'PostgreSQL schema', + 'Snap2SQL', + 'ER diagram parser' + ], openGraph: { title, description, url: '/', - siteName: 'vdbs', + siteName: 'snap2sql', locale: 'en_US', type: 'website', images: [ @@ -37,11 +48,12 @@ export const metadata: Metadata = { } } -export default function RootLayout({ children }: { children: React.ReactNode }): JSX.Element { +export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - -
+
{children} diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 8ad6487..cb9ec63 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,91 +1,98 @@ 'use client' import { useEffect, useRef, useState } from 'react' -import { Loader2, TableRowsSplit } from 'lucide-react' -import { toast } from 'sonner' -import { useCompletion } from '@ai-sdk/react' +import { useRouter } from 'next/navigation' import Image from 'next/image' +import { UploadIcon } from 'lucide-react' +import { toast } from 'sonner' + import { cn } from '@/lib/utils' -import { isSupportedImageType, nanoid, toBase64 } from '@/utils' +import { isSupportedImageType, toBase64 } from '@/utils' + import { useSchemaStore } from '@/store' -import { saveGeneration } from '@/actions' -import { Results } from '@/components/results' -const LIMIT_MB = 1.5 * 1024 * 1024 +import { Header } from '@/components/header' +import { DatabasePicker } from '@/components/database-picker' + +const LIMIT_MB = 2 * 1024 * 1024 -export default function Page(): JSX.Element { +export default function Page() { const [isDraggingOver, setIsDraggingOver] = useState(false) const inputRef = useRef(null) const [blobURL, setBlobURL] = useState(null) - const [finished, setFinished] = useState(false) + const [isLoading, setIsLoading] = useState(false) + const [databaseFormat, setDatabaseFormat] = useState('postgresql') const setSchema = useSchemaStore((state) => state.setSchema) - const setSupabaseLinkTables = useSchemaStore((state) => state.setSupabaseLinkTables) - const { complete, completion, isLoading } = useCompletion({ - api: 'api/code-generation', - onFinish: (_, completion) => { - if (!completion) { - return + const router = useRouter() + + const getGenerationAI = async (base64: string) => { + const toastId = toast.loading('Generation Database Schema') + + try { + setIsLoading(true) + const response = await fetch('api/gemini-generation', { + method: 'POST', + body: JSON.stringify({ prompt: base64, databaseFormat }), + headers: { + 'Content-type': 'application/json' + } + }) + if (response.status === 429) { + throw new Error('You have reached your request limit for the day.') + } else if (response.status !== 200) { + throw new Error('An error has ocurred while generation database schema') } + const results = await response.json() + const { sqlSchema, tables } = results.data + if ( - completion.trim() === 'Invalid SQL diagram.' || - !completion.includes('CREATE TABLE') || - !completion.includes('--TABLE') + sqlSchema.trim() === 'Invalid SQL diagram.' || + !sqlSchema.includes('CREATE TABLE') + // !sqlSchema.includes('--TABLE') ) { toast.error('This is not a valid SQL diagram. Please try again.') return } - const sqlSchema = completion - .split('--TABLE\n') - .filter((table: string) => table !== '') - .join('\n') - .trim() - - const data = { + const schema = { sqlSchema, - cmdCode: nanoid() + tables, + databaseFormat: databaseFormat as string } - // Saving in db the generation - toast.promise(saveGeneration(data), { - loading: 'Saving Generation...', - success: () => { - setFinished(true) - setSchema(data) - setSupabaseLinkTables(undefined) - return `Generation saved successfully.` - }, - error: 'An error has ocurred while saving data.' - }) - }, - onError: (err) => { - const result = JSON.parse(err.message) - toast.error(result.message) - - setBlobURL(null) - setFinished(true) + setSchema(schema) + setTimeout(() => { + router.push('/results') + }, 1000) + } catch (error) { + if (error instanceof Error) { + toast.error(error.message) + } + } finally { + setIsLoading(false) + toast.dismiss(toastId) } - }) + } const submit = async (file?: File | Blob) => { if (!file) return if (!isSupportedImageType(file.type)) { - return toast.error('Unsupported format. Only JPEG, PNG, GIF, and WEBP files are supported.') + return toast.error('Unsupported format. Only JPEG, PNG, and WEBP files are supported.') } if (file.size > LIMIT_MB) return toast.error('Image too large, maximum file size is 1MB.') const base64 = await toBase64(file) - // if (base64.length > 2_333_333) { - // return toast.error("Image too large, maximum file size is 1MB."); - // } + if (!databaseFormat) { + toast.error(`You haven't selected a database format`) + return + } setBlobURL(URL.createObjectURL(file)) - setFinished(false) - complete(base64) + await getGenerationAI(base64) } const handleDragLeave = () => { @@ -133,46 +140,45 @@ export default function Page(): JSX.Element { }, []) return ( - <> -
inputRef.current?.click()} - > - {blobURL && ( - Uploaded image - )} - +
+
+
+
inputRef.current?.click()} > - {isLoading ? ( - + {blobURL ? ( + Uploaded image ) : ( - <> - -

- DB Image to SQL Schema +

+
+ +
+

+ Drop or paste anywhere, or click to upload

-

- Drop or paste anywhere, or click to upload. +

+ Supports PNG, JPEG, and JPG (max 2MB)

- +
)} +
-
- {(isLoading || completion) && ( -
- -
- )} - +
) } diff --git a/apps/web/app/results/page.tsx b/apps/web/app/results/page.tsx new file mode 100644 index 0000000..62d0e8e --- /dev/null +++ b/apps/web/app/results/page.tsx @@ -0,0 +1,26 @@ +import { UploadIcon } from 'lucide-react' +import Link from 'next/link' +import { Header } from '@/components/header' +import { OptionsResults } from '@/components/options-results' +import { SchemaResults } from '@/components/schema-results' +import { Button } from '@/components/ui/button' + +export default function Results() { + return ( +
+
+ +
+ +
+ + +
+
+ ) +} diff --git a/apps/web/components/app-logo.tsx b/apps/web/components/app-logo.tsx index 020e970..5d02aee 100644 --- a/apps/web/components/app-logo.tsx +++ b/apps/web/components/app-logo.tsx @@ -1,12 +1,11 @@ -import Image from 'next/image' import Link from 'next/link' export function AppLogo() { return ( - - Application logo - - vdbs + + Snap2SQL logo + + Snap2SQL ) diff --git a/apps/web/components/code-command.tsx b/apps/web/components/code-command.tsx deleted file mode 100644 index bcad8e0..0000000 --- a/apps/web/components/code-command.tsx +++ /dev/null @@ -1,7 +0,0 @@ -type CommandCodeProps = { - commandCode: string -} - -export function CodeCommand({ commandCode }: CommandCodeProps) { - return {commandCode} -} diff --git a/apps/web/components/code-editor.tsx b/apps/web/components/code-editor.tsx index be37678..597fb8a 100644 --- a/apps/web/components/code-editor.tsx +++ b/apps/web/components/code-editor.tsx @@ -91,20 +91,20 @@ export function CodeEditor({ code }: CodeEditorProps) { height='100%' theme='vs-dark' value={code} - className='lg:h-[calc(100vh-233px)]' + className='lg:h-[calc(100vh-250px)]' loading={
} options={{ - readOnly: false, + readOnly: true, padding: { top: 20 }, - cursorSmoothCaretAnimation: 'on', + cursorSmoothCaretAnimation: 'off', language: 'sql', - cursorBlinking: 'smooth', + cursorBlinking: 'solid', fontSize: 16, formatOnType: true, formatOnPaste: true, diff --git a/apps/web/components/database-deployments.tsx b/apps/web/components/database-deployments.tsx new file mode 100644 index 0000000..2ddfd42 --- /dev/null +++ b/apps/web/components/database-deployments.tsx @@ -0,0 +1,249 @@ +import React, { FormEvent, useState } from 'react' +import { AlertCircleIcon, Loader2Icon } 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 { + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + CardFooter +} from '@/components/ui/card' +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' +import { SupabaseIc, NeonIc } from '@/components/icons' +import { useSchemaStore } from '@/store' +import { triggerConfetti } from '@/utils' +import { + validateNeonConnectionString, + validateSupabaseConnectionString +} from '@/utils/connection-string-validations' + +const providers = [ + { + id: 'neon', + name: 'Neon', + icon: NeonIc, + description: 'Ship faster with Postgres', + color: 'text-green-600 dark:text-green-400', + bgColor: 'bg-green-50 dark:bg-green-900/20', + borderColor: 'border-green-200 dark:border-green-800' + }, + { + id: 'supabase', + name: 'Supabase', + icon: SupabaseIc, + description: 'Open source Firebase alternative', + color: 'text-emerald-600 dark:text-emerald-400', + bgColor: 'bg-emerald-50 dark:bg-emerald-900/20', + borderColor: 'border-emerald-200 dark:border-emerald-800' + } + // { + // id: 'planetscale', + // name: 'PlanetScale', + // icon: GlobeIcon, + // description: `The world's fastest relational database`, + // color: 'text-purple-600 dark:text-purple-400', + // bgColor: 'bg-purple-50 dark:bg-purple-900/20', + // borderColor: 'border-purple-200 dark:border-purple-800' + // }, + // { + // id: 'turso', + // name: 'Turso', + // icon: TursoIc, + // description: 'SQLite Databases for all Apps', + // color: 'text-blue-600 dark:text-blue-400', + // bgColor: 'bg-blue-50 dark:bg-blue-900/20', + // borderColor: 'border-blue-200 dark:border-blue-800' + // } +] + +export function DatabaseDeployments() { + const [selectedProvider, setSelectedProvider] = useState('neon') + const [isDeploying, setIsDeploying] = useState(false) + const schema = useSchemaStore((store) => store.schema) + + if (schema && schema.databaseFormat !== 'postgresql') return null + + const isConnectionStringValid = (connectionString: string) => { + if (!connectionString.trim() || !schema) { + toast.error('Database connection string is missing.') + return false + } + + if (selectedProvider === 'neon') { + const { isValid, errorMessage } = validateNeonConnectionString(connectionString) + if (!isValid) { + toast.error(errorMessage) + return false + } + } else if (selectedProvider === 'supabase') { + const { isValid, errorMessage } = validateSupabaseConnectionString(connectionString) + if (!isValid) { + toast.error(errorMessage) + return false + } + } + + return true + } + + const handleDeploy = async (e: FormEvent) => { + e.preventDefault() + + const form = e.currentTarget + const connectionStringInput = form.elements.namedItem('connectionString') as HTMLInputElement + const connectionString = connectionStringInput.value + + const isValid = isConnectionStringValid(connectionString) + if (!isValid) return + + setIsDeploying(true) + + try { + const response = await fetch('api/deploy', { + method: 'POST', + body: JSON.stringify({ + url: connectionString, + sqlSchema: schema!.sqlSchema + }), + headers: { + 'Content-type': 'application/json' + } + }) + const data = await response.json() + const { error, message } = data + if (error) { + throw new Error(error) + } + + toast.message(message) + + triggerConfetti() + } catch (error) { + if (error instanceof Error) { + toast.error(error.message) + } + } finally { + setIsDeploying(false) + form.reset() + } + } + + const getPlaceholder = () => { + switch (selectedProvider) { + case 'neon': + return 'postgresql://[ROLE]:[PASSWORD]@[INSTANCE].west-001.aws.neon.tech/neondb' + case 'supabase': + return 'postgresql://postgres:[YOUR-PASSWORD]@db.[YOUR-SUPABASE-ID].supabase.co:5432/postgres' + default: + return 'Select a provider first' + } + } + + // const testConnection = async () => { + // const isValid = isConnectionStringValid() + // if (!isValid) return + + // const connectionString = inputRef.current?.value + + // setIsTesting(true) + + // try { + // const response = await fetch('api/test-connection', { + // method: 'POST', + // body: JSON.stringify({ + // url: connectionString, + // provider: selectedProvider + // }), + // headers: { + // 'Content-type': 'application/json' + // } + // }) + // const data = await response.json() + // console.log(data) + // } catch (error) { + // toast.error('An error has ocurred while deploying data') + // } finally { + // setIsTesting(false) + // } + // } + + return ( +
+ + + Deploy Schema + + + +
+ {providers.map((provider) => { + const Icon = provider.icon + return ( + + ) + })} +
+ {selectedProvider && ( + <> +
+ + +
+ + + + + + We never store your database credentials. They are used exclusively for this + connection. + + + + )} +
+ +
+ +
+
+
+
+ ) +} diff --git a/apps/web/components/database-picker.tsx b/apps/web/components/database-picker.tsx new file mode 100644 index 0000000..4c2b2bd --- /dev/null +++ b/apps/web/components/database-picker.tsx @@ -0,0 +1,62 @@ +'use client' +import { Dispatch, SetStateAction } from 'react' +import { MySQLIc, PostgreSQLIc, SQLiteIc } from '@/components/icons' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' + +const schemas = [ + { + id: 'postgresql', + title: 'PostgreSQL', + Icon: , + description: 'Advanced open-source relational database with strong SQL compliance.' + }, + { + id: 'mysql', + title: 'MySQL', + Icon: , + description: 'Popular open-source relational database management system.' + }, + { + id: 'sqlite', + title: 'SQLite', + Icon: , + description: 'Popular open-source relational database management system.' + } +] + +type DatabasePickerProps = { + databaseFormat: string + setDatabaseFormat: Dispatch> +} + +export function DatabasePicker({ databaseFormat, setDatabaseFormat }: DatabasePickerProps) { + return ( + + + Choose Database Schema + + + +
+ {schemas.map((option) => ( +
setDatabaseFormat(option.id)} + > +
+ {option.Icon} +

{option.title}

+

{option.description}

+
+
+ ))} +
+
+
+ ) +} diff --git a/apps/web/components/deploy-result.tsx b/apps/web/components/deploy-result.tsx deleted file mode 100644 index 54ec39e..0000000 --- a/apps/web/components/deploy-result.tsx +++ /dev/null @@ -1,33 +0,0 @@ -'use client' - -import { ExternalLink } from 'lucide-react' - -type DeployResultProps = { - supabaseLinkTables: string -} - -export function DeployResult({ supabaseLinkTables }: DeployResultProps) { - return ( -
-

View the results on the following link:

-
-
-
- {supabaseLinkTables} - - - -
-
-
-
- ) -} diff --git a/apps/web/components/deploy-schema.tsx b/apps/web/components/deploy-schema.tsx deleted file mode 100644 index dcec1fa..0000000 --- a/apps/web/components/deploy-schema.tsx +++ /dev/null @@ -1,136 +0,0 @@ -'use client' - -import { useRef, useState } from 'react' -import { toast } from 'sonner' -import Link from 'next/link' -import { CircleAlert, Loader2 } from 'lucide-react' -import { getReferenceId, triggerConfetti } from '@/utils' -import { useSchemaStore } from '@/store' -import { schemaDeploy } from '@/services/deploy' -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle -} from '@/components/ui/card' -import { Button } from '@/components/ui/button' -import { Label } from '@/components/ui/label' -import { Input } from '@/components/ui/input' -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' -import { DeployResult } from '@/components/deploy-result' - -export function DeploySchema() { - const schema = useSchemaStore((state) => state.schema) - const setSupabaseLinkTables = useSchemaStore((state) => state.setSupabaseLinkTables) - const supabaseLinkTables = useSchemaStore((state) => state.supabaseLinkTables) - const inputRef = useRef(null) - const [isLoading, setIsLoading] = useState(false) - - const deploy = async () => { - const urlConnection = inputRef.current?.value - if (!urlConnection) { - toast.error('Database connection string is missing.') - return - } - - const isEmptyPassword = urlConnection.includes('[YOUR-PASSWORD]') - - if (!schema) return - - const { sqlSchema } = schema - - if (sqlSchema.trim() === '' || isEmptyPassword) { - toast.error('Please replace [YOUR-PASSWORD] with your actual database password.') - return - } - - setIsLoading(true) - const result = await schemaDeploy({ - sqlSchema, - url: urlConnection - }) - - const { error, message } = result - if (error) { - toast.error(error) - setIsLoading(false) - return - } - - toast.success(message) - triggerConfetti() - setIsLoading(false) - - //Setting redirect URL dashboard tables - const referenceId = getReferenceId(urlConnection) - const supabaseLinkTables = `https://supabase.com/dashboard/project/${referenceId}/editor` - setSupabaseLinkTables(supabaseLinkTables) - } - - return ( - - - Connect project - Deploy your migration script to your Supabase project. - - -
-
-
-
- - -
-

- You can find your Supabase database connection URL in your{' '} - - database settings - {' '} - in the Supabase dashboard. -

-
- - - - We never store your database credentials - - - The credentials you provide are used exclusively for validating your database - connection. You can examine the{' '} - - source code - {' '} - for verification. - - -
-
-
- - - {supabaseLinkTables && supabaseLinkTables !== '' && ( - - )} - -
- ) -} diff --git a/apps/web/components/footer.tsx b/apps/web/components/footer.tsx index fa5ae0e..8c0d1f5 100644 --- a/apps/web/components/footer.tsx +++ b/apps/web/components/footer.tsx @@ -1,68 +1,12 @@ -import { SVGProps } from 'react' - -function GitIc(props: SVGProps) { - return ( - - - - - ) -} - -function TwitterIc(props: SVGProps) { - return ( - - - - ) -} - export function Footer() { return ( -