From b9cc8c06907d78a8c43b1c473a4ca84acefafc1b Mon Sep 17 00:00:00 2001 From: "maxim.budnik" Date: Sun, 20 Apr 2025 21:35:54 +0300 Subject: [PATCH 1/6] onboarding modal layout --- .../(layout)/components/OnboardingModal.tsx | 127 ++++++++++++++++++ .../dashboard/src/app/(layout)/layout.tsx | 2 + 2 files changed, 129 insertions(+) create mode 100644 packages/dashboard/src/app/(layout)/components/OnboardingModal.tsx diff --git a/packages/dashboard/src/app/(layout)/components/OnboardingModal.tsx b/packages/dashboard/src/app/(layout)/components/OnboardingModal.tsx new file mode 100644 index 0000000..a827fbe --- /dev/null +++ b/packages/dashboard/src/app/(layout)/components/OnboardingModal.tsx @@ -0,0 +1,127 @@ +"use client" + +import {useEffect, useState} from "react" +import {Button} from "@/components/ui/button" +import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card" +import {Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle} from "@/components/ui/dialog" +import {ArrowRight, Download, PlayCircle, Settings} from "lucide-react" +import {useRouter} from "next/navigation"; + +export default function OnboardingModal() { + const router = useRouter() + + const [isOpen, setIsOpen] = useState(false) + + // Show the modal when the component mounts (for new users) + useEffect(() => { + // In a real app, you'd check if the user is new before showing the modal + // For example: if (localStorage.getItem('isNewUser') === 'true') + setIsOpen(true) + + // You could set a flag to not show this again + // localStorage.setItem('isNewUser', 'false') + }, []) + + const onDemoProject = () =>{ + + setIsOpen(false) + } + + const onImport = ()=>{ + router.push("/services/import") + setIsOpen(false) + } + + const onManual = ()=>{ + router.push("/services/new") + setIsOpen(false) + + } + + + return ( + + + + Welcome to API 200 + + Choose one of the following options to get started with your API journey. + + +
+ +
+ + + + Start with demo project + + + + + Get up and running quickly with a pre-configured demo project that showcases the + capabilities of API 200. + + +
+
+ +
+
+ + +
+ + + + Import API + + + + + Import your existing API from OpenAPI/Swagger, Postman, or other formats to get started + quickly. + + +
+
+ +
+
+ + +
+ + + + Create service manually + + + + + Build your API service from scratch with full control over all settings and + configurations. + + +
+
+ +
+
+
+ +
+ +
+
+
+ ) +} diff --git a/packages/dashboard/src/app/(layout)/layout.tsx b/packages/dashboard/src/app/(layout)/layout.tsx index ab3c18f..abdfb6f 100644 --- a/packages/dashboard/src/app/(layout)/layout.tsx +++ b/packages/dashboard/src/app/(layout)/layout.tsx @@ -2,6 +2,7 @@ import { AppSidebar } from "@/app/(layout)/components/AppSidebar" import { SidebarProvider } from '@/components/ui/sidebar' import { redirect } from 'next/navigation' import {createClient} from "@/utils/supabase/server"; +import OnboardingModal from "@/app/(layout)/components/OnboardingModal"; export default async function RootLayout({ children, @@ -19,6 +20,7 @@ export default async function RootLayout({
+
{children}
From 2e39e3ddb8252082f0daf7a9d499a7b5ab4ae6e3 Mon Sep 17 00:00:00 2001 From: "maxim.budnik" Date: Sun, 20 Apr 2025 21:52:12 +0300 Subject: [PATCH 2/6] onboarding modal - demo project --- packages/dashboard/package.json | 2 +- .../(layout)/components/OnboardingModal.tsx | 96 ++- .../dashboard/src/utils/data/demoSwagger.ts | 699 ++++++++++++++++++ .../src/utils/supabase/database.types.ts | 14 +- 4 files changed, 791 insertions(+), 20 deletions(-) create mode 100644 packages/dashboard/src/utils/data/demoSwagger.ts diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json index 395a2bf..40150fa 100644 --- a/packages/dashboard/package.json +++ b/packages/dashboard/package.json @@ -38,7 +38,7 @@ "clsx": "^2.1.1", "date-fns": "^4.1.0", "lucide-react": "^0.475.0", - "next": "15.2.3", + "next": "15.2.4", "next-runtime-env": "^3.2.2", "next-themes": "^0.4.6", "nextjs-toploader": "^3.8.15", diff --git a/packages/dashboard/src/app/(layout)/components/OnboardingModal.tsx b/packages/dashboard/src/app/(layout)/components/OnboardingModal.tsx index a827fbe..a2551a8 100644 --- a/packages/dashboard/src/app/(layout)/components/OnboardingModal.tsx +++ b/packages/dashboard/src/app/(layout)/components/OnboardingModal.tsx @@ -5,12 +5,17 @@ import {Button} from "@/components/ui/button" import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card" import {Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle} from "@/components/ui/dialog" import {ArrowRight, Download, PlayCircle, Settings} from "lucide-react" -import {useRouter} from "next/navigation"; +import {useRouter} from "next/navigation" +import {toast} from "sonner" +import {createClient} from "@/utils/supabase/client" +import {parseSwagger} from "@/app/(layout)/services/import/parseSwagger" +import {demoSwagger} from "@/utils/data/demoSwagger" export default function OnboardingModal() { const router = useRouter() - + const supabase = createClient() const [isOpen, setIsOpen] = useState(false) + const [isLoading, setIsLoading] = useState(false) // Show the modal when the component mounts (for new users) useEffect(() => { @@ -22,23 +27,60 @@ export default function OnboardingModal() { // localStorage.setItem('isNewUser', 'false') }, []) - const onDemoProject = () =>{ + const onDemoProject = async () => { + try { + setIsLoading(true) + const demoSwaggerString = JSON.stringify(demoSwagger) + const parsedResult = parseSwagger(demoSwaggerString) + const {data: serviceData, error: serviceError} = await supabase + .from('services') + .insert({ + ...parsedResult.service, + is_mcp_enabled: true, + name: "Demo Project", + description: "Demo project for JSON Placeholder API with enabled MCP Server", + }) + .select() + .single() - setIsOpen(false) + if (serviceError) { + throw serviceError + } + const endpointsToInsert = parsedResult.endpoints.map(e => ({ + ...e, + service_id: serviceData.id + })) + const {error: endpointsError} = await supabase + .from('endpoints') + .insert(endpointsToInsert) + + if (endpointsError) { + throw endpointsError + } + setIsOpen(false) + toast.success("Demo project created successfully!") + router.push(`/services/${serviceData.id}`) + + } catch (error) { + console.error("Error creating demo project:", error) + toast.error("Failed to create demo project", { + description: error instanceof Error ? error.message : "Unknown error" + }) + } finally { + setIsLoading(false) + } } - const onImport = ()=>{ + const onImport = () => { router.push("/services/import") setIsOpen(false) } - const onManual = ()=>{ + const onManual = () => { router.push("/services/new") setIsOpen(false) - } - return ( @@ -49,7 +91,10 @@ export default function OnboardingModal() {
- +
@@ -65,13 +110,19 @@ export default function OnboardingModal() {
-
- +
@@ -81,19 +132,24 @@ export default function OnboardingModal() { - Import your existing API from OpenAPI/Swagger, Postman, or other formats to get started + Import your existing API from OpenAPI/Swagger, Postman, or other formats to get + started quickly.
-
- +
@@ -109,7 +165,7 @@ export default function OnboardingModal() {
-
@@ -117,7 +173,13 @@ export default function OnboardingModal() {
-
diff --git a/packages/dashboard/src/utils/data/demoSwagger.ts b/packages/dashboard/src/utils/data/demoSwagger.ts new file mode 100644 index 0000000..de97eef --- /dev/null +++ b/packages/dashboard/src/utils/data/demoSwagger.ts @@ -0,0 +1,699 @@ +export const demoSwagger = { + "swagger":"2.0", + "info":{ + "description":"Fake Online REST API for Testing and Prototyping", + "version":"1.0.0", + "title":"JSON Placeholder" + }, + "host":"jsonplaceholder.typicode.com", + "tags":[ + { + "name":"posts" + }, + { + "name":"comments" + }, + { + "name":"albums" + }, + { + "name":"photos" + }, + { + "name":"todos" + }, + { + "name":"users" + } + ], + "schemes":[ + "https" + ], + "paths":{ + "/posts":{ + "get":{ + "tags":[ + "posts" + ], + "operationId":"getPosts", + "summary":"Get all available posts", + "parameters":[ + { + "name":"id", + "in":"query", + "type":"integer", + "description":"Filter by post ID", + "required":false + }, + { + "name":"userId", + "in":"query", + "type":"integer", + "description":"Filter by user ID", + "required":false + } + ], + "produces":[ + "application/json" + ], + "responses":{ + "200":{ + "description":"successful operation", + "schema":{ + "type":"array", + "items":{ + "$ref":"#/definitions/Post" + } + } + } + } + } + }, + "/posts/{id}":{ + "get":{ + "tags":[ + "posts" + ], + "summary":"Get specific post", + "parameters":[ + { + "name":"id", + "in":"path", + "description":"The ID of the post to retrieve", + "required":true, + "type":"integer" + } + ], + "responses":{ + "200":{ + "description":"successful operation", + "schema":{ + "$ref":"#/definitions/Post" + } + }, + "404":{ + "description":"not found", + "schema":{ + "$ref":"#/definitions/NotFoundError" + } + } + } + } + }, + "/posts/{id}/comments":{ + "get":{ + "tags":[ + "posts" + ], + "summary":"Get comments for a specific post", + "parameters":[ + { + "name":"id", + "in":"path", + "description":"post id", + "required":true, + "type":"integer" + } + ], + "responses":{ + "200":{ + "description":"successful operation", + "schema":{ + "type":"array", + "items":{ + "$ref":"#/definitions/Comment" + } + } + }, + "404":{ + "description":"not found", + "schema":{ + "$ref":"#/definitions/NotFoundError" + } + } + } + } + }, + "/comments":{ + "get":{ + "tags":[ + "comments" + ], + "operationId":"getComments", + "summary":"Get all available comments", + "parameters":[ + { + "name":"id", + "in":"query", + "type":"integer", + "description":"Filter by comment ID", + "required":false + }, + { + "name":"postId", + "in":"query", + "type":"integer", + "description":"Filter by post ID", + "required":false + } + ], + "produces":[ + "application/json" + ], + "responses":{ + "200":{ + "description":"successful operation", + "schema":{ + "type":"array", + "items":{ + "$ref":"#/definitions/Comment" + } + } + } + } + } + }, + "/comments/{id}":{ + "get":{ + "tags":[ + "comments" + ], + "operationId":"getComment", + "summary":"Get specific comment", + "parameters":[ + { + "name":"id", + "in":"path", + "description":"The ID of the comment to retrieve", + "required":true, + "type":"integer" + } + ], + "responses":{ + "200":{ + "description":"successful operation", + "schema":{ + "$ref":"#/definitions/Comment" + } + }, + "404":{ + "description":"not found", + "schema":{ + "$ref":"#/definitions/NotFoundError" + } + } + } + } + }, + "/albums":{ + "get":{ + "tags":[ + "albums" + ], + "operationId":"getAlbums", + "summary":"Get all available albums", + "parameters":[ + { + "name":"id", + "in":"query", + "type":"integer", + "description":"Filter by album ID", + "required":false + }, + { + "name":"userId", + "in":"query", + "type":"integer", + "description":"Filter by user ID", + "required":false + } + ], + "produces":[ + "application/json" + ], + "responses":{ + "200":{ + "description":"successful operation", + "schema":{ + "type":"array", + "items":{ + "$ref":"#/definitions/Album" + } + } + } + } + } + }, + "/albums/{id}":{ + "get":{ + "tags":[ + "albums" + ], + "summary":"Get specific album", + "parameters":[ + { + "name":"id", + "in":"path", + "description":"The ID of the album to retrieve", + "required":true, + "type":"integer" + } + ], + "responses":{ + "200":{ + "description":"successful operation", + "schema":{ + "$ref":"#/definitions/Album" + } + }, + "404":{ + "description":"not found", + "schema":{ + "$ref":"#/definitions/NotFoundError" + } + } + } + } + }, + "/albums/{id}/photos":{ + "get":{ + "tags":[ + "albums" + ], + "summary":"Get photos for a specific album", + "parameters":[ + { + "name":"id", + "in":"path", + "description":"post id", + "required":true, + "type":"integer" + } + ], + "responses":{ + "200":{ + "description":"successful operation", + "schema":{ + "type":"array", + "items":{ + "$ref":"#/definitions/Photo" + } + } + }, + "404":{ + "description":"not found", + "schema":{ + "$ref":"#/definitions/NotFoundError" + } + } + } + } + }, + "/photos":{ + "get":{ + "tags":[ + "photos" + ], + "operationId":"getPhotos", + "summary":"Get all available photos", + "parameters":[ + { + "name":"id", + "in":"query", + "type":"integer", + "description":"Filter by photo ID", + "required":false + }, + { + "name":"albumId", + "in":"query", + "type":"integer", + "description":"Filter by album ID", + "required":false + } + ], + "produces":[ + "application/json" + ], + "responses":{ + "200":{ + "description":"successful operation", + "schema":{ + "type":"array", + "items":{ + "$ref":"#/definitions/Photo" + } + } + } + } + } + }, + "/photos/{id}":{ + "get":{ + "tags":[ + "photos" + ], + "operationId":"getPhoto", + "summary":"Get specific photo", + "parameters":[ + { + "name":"id", + "in":"path", + "description":"The ID of the photo to retrieve", + "required":true, + "type":"integer" + } + ], + "responses":{ + "200":{ + "description":"successful operation", + "schema":{ + "$ref":"#/definitions/Photo" + } + }, + "404":{ + "description":"not found", + "schema":{ + "$ref":"#/definitions/NotFoundError" + } + } + } + } + }, + "/todos":{ + "get":{ + "tags":[ + "todos" + ], + "operationId":"getTodos", + "summary":"Get all available todos", + "parameters":[ + { + "name":"id", + "in":"query", + "type":"integer", + "description":"Filter by todo ID", + "required":false + }, + { + "name":"userId", + "in":"query", + "type":"integer", + "description":"Filter by user ID", + "required":false + } + ], + "produces":[ + "application/json" + ], + "responses":{ + "200":{ + "description":"successful operation", + "schema":{ + "type":"array", + "items":{ + "$ref":"#/definitions/Todo" + } + } + } + } + } + }, + "/todos/{id}":{ + "get":{ + "tags":[ + "todos" + ], + "operationId":"getTodo", + "summary":"Get specific todo", + "parameters":[ + { + "name":"id", + "in":"path", + "description":"The ID of the todo to retrieve", + "required":true, + "type":"integer" + } + ], + "responses":{ + "200":{ + "description":"successful operation", + "schema":{ + "$ref":"#/definitions/Todo" + } + }, + "404":{ + "description":"not found", + "schema":{ + "$ref":"#/definitions/NotFoundError" + } + } + } + } + }, + "/users":{ + "get":{ + "tags":[ + "users" + ], + "operationId":"getUsers", + "summary":"Get all available users", + "parameters":[ + { + "name":"id", + "in":"query", + "type":"integer", + "description":"Filter by user ID", + "required":false + }, + { + "name":"email", + "in":"query", + "type":"integer", + "description":"Filter by user email address", + "required":false + } + ], + "produces":[ + "application/json" + ], + "responses":{ + "200":{ + "description":"successful operation", + "schema":{ + "type":"array", + "items":{ + "$ref":"#/definitions/User" + } + } + } + } + } + }, + "/users/{id}":{ + "get":{ + "tags":[ + "users" + ], + "operationId":"getUser", + "summary":"Get specific user", + "parameters":[ + { + "name":"id", + "in":"path", + "description":"The ID of the user to retrieve", + "required":true, + "type":"integer" + } + ], + "responses":{ + "200":{ + "description":"successful operation", + "schema":{ + "$ref":"#/definitions/User" + } + }, + "404":{ + "description":"not found", + "schema":{ + "$ref":"#/definitions/NotFoundError" + } + } + } + } + } + }, + "definitions":{ + "Post":{ + "type":"object", + "properties":{ + "id":{ + "type":"integer", + "format":"int64" + }, + "userId":{ + "type":"integer", + "format":"int64" + }, + "title":{ + "type":"string" + }, + "body":{ + "type":"string" + } + } + }, + "Comment":{ + "type":"object", + "properties":{ + "id":{ + "type":"integer", + "format":"int64" + }, + "postId":{ + "type":"integer", + "format":"int64" + }, + "name":{ + "type":"string" + }, + "email":{ + "type":"string", + "format":"email" + }, + "body":{ + "type":"string" + } + } + }, + "Album":{ + "type":"object", + "properties":{ + "id":{ + "type":"integer", + "format":"int64" + }, + "userId":{ + "type":"integer", + "format":"int64" + }, + "title":{ + "type":"string" + } + } + }, + "Photo":{ + "type":"object", + "properties":{ + "id":{ + "type":"integer", + "format":"int64" + }, + "albumId":{ + "type":"integer", + "format":"int64" + }, + "title":{ + "type":"string" + }, + "url":{ + "type":"string", + "format":"uri" + }, + "thumbnailUrl":{ + "type":"string", + "format":"uri" + } + } + }, + "Todo":{ + "type":"object", + "properties":{ + "id":{ + "type":"integer", + "format":"int64" + }, + "userId":{ + "type":"integer", + "format":"int64" + }, + "title":{ + "type":"string" + }, + "completed":{ + "type":"boolean" + } + } + }, + "User":{ + "type":"object", + "properties":{ + "id":{ + "type":"integer", + "format":"int64" + }, + "name":{ + "type":"string" + }, + "username":{ + "type":"string" + }, + "email":{ + "type":"string", + "format":"email" + }, + "phone":{ + "type":"string" + }, + "website":{ + "type":"string" + }, + "company":{ + "type":"object", + "properties":{ + "name":{ + "type":"string" + }, + "catchPhrase":{ + "type":"string" + }, + "bs":{ + "type":"string" + } + } + }, + "address":{ + "type":"object", + "properties":{ + "street":{ + "type":"string" + }, + "suite":{ + "type":"string" + }, + "city":{ + "type":"string" + }, + "zipcode":{ + "type":"string" + }, + "geo":{ + "type":"object", + "properties":{ + "lat":{ + "type":"string" + }, + "lng":{ + "type":"string" + } + } + } + } + } + } + }, + "NotFoundError":{ + "type":"object" + } + } +} diff --git a/packages/dashboard/src/utils/supabase/database.types.ts b/packages/dashboard/src/utils/supabase/database.types.ts index e615b45..c3386fd 100644 --- a/packages/dashboard/src/utils/supabase/database.types.ts +++ b/packages/dashboard/src/utils/supabase/database.types.ts @@ -135,13 +135,13 @@ export type Database = { Insert: { created_at?: string | null endpoint_id: number - id?: never + id?: number schema?: Json | null } Update: { created_at?: string | null endpoint_id?: number - id?: never + id?: number schema?: Json | null } Relationships: [ @@ -316,18 +316,21 @@ export type Database = { billing_started_at: string calls_count: number id: number + mcp_calls_count: number user_id: string } Insert: { billing_started_at?: string calls_count?: number id?: number + mcp_calls_count?: number user_id: string } Update: { billing_started_at?: string calls_count?: number id?: number + mcp_calls_count?: number user_id?: string } Relationships: [] @@ -346,6 +349,13 @@ export type Database = { } Returns: Json } + increment_mcp_usage: { + Args: { p_user_id: string; p_max_requests: number } + Returns: { + allowed: boolean + c_count: number + }[] + } increment_usage: { Args: { p_user_id: string; p_max_requests: number } Returns: { From 270ee5e735f8838bee3d22dce693c3ba6e8b451c Mon Sep 17 00:00:00 2001 From: "maxim.budnik" Date: Sun, 20 Apr 2025 22:08:50 +0300 Subject: [PATCH 3/6] onboarding modal - checks to show modal once --- packages/dashboard/pnpm-lock.yaml | 106 +++++++++--------- .../(layout)/components/OnboardingModal.tsx | 49 ++++++-- 2 files changed, 94 insertions(+), 61 deletions(-) diff --git a/packages/dashboard/pnpm-lock.yaml b/packages/dashboard/pnpm-lock.yaml index 639f417..ce06fb9 100644 --- a/packages/dashboard/pnpm-lock.yaml +++ b/packages/dashboard/pnpm-lock.yaml @@ -61,7 +61,7 @@ importers: version: 1.1.8(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@sentry/nextjs': specifier: ^9.9.0 - version: 9.9.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.2.3(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(webpack@5.98.0) + version: 9.9.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(webpack@5.98.0) '@supabase/ssr': specifier: ^0.5.2 version: 0.5.2(@supabase/supabase-js@2.49.3) @@ -73,7 +73,7 @@ importers: version: 8.21.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@vercel/analytics': specifier: ^1.5.0 - version: 1.5.0(next@15.2.3(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) + version: 1.5.0(next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) cache-manager: specifier: ^6.4.1 version: 6.4.1 @@ -90,17 +90,17 @@ importers: specifier: ^0.475.0 version: 0.475.0(react@19.0.0) next: - specifier: 15.2.3 - version: 15.2.3(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: 15.2.4 + version: 15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next-runtime-env: specifier: ^3.2.2 - version: 3.2.2(next@15.2.3(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) + version: 3.2.2(next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0) nextjs-toploader: specifier: ^3.8.15 - version: 3.8.15(next@15.2.3(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 3.8.15(next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: specifier: ^19.0.0 version: 19.0.0 @@ -484,56 +484,56 @@ packages: '@napi-rs/wasm-runtime@0.2.7': resolution: {integrity: sha512-5yximcFK5FNompXfJFoWanu5l8v1hNGqNHh9du1xETp9HWk/B/PzvchX55WYOPaIeNglG8++68AAiauBAtbnzw==} - '@next/env@15.2.3': - resolution: {integrity: sha512-a26KnbW9DFEUsSxAxKBORR/uD9THoYoKbkpFywMN/AFvboTt94b8+g/07T8J6ACsdLag8/PDU60ov4rPxRAixw==} + '@next/env@15.2.4': + resolution: {integrity: sha512-+SFtMgoiYP3WoSswuNmxJOCwi06TdWE733D+WPjpXIe4LXGULwEaofiiAy6kbS0+XjM5xF5n3lKuBwN2SnqD9g==} '@next/eslint-plugin-next@15.1.6': resolution: {integrity: sha512-+slMxhTgILUntZDGNgsKEYHUvpn72WP1YTlkmEhS51vnVd7S9jEEy0n9YAMcI21vUG4akTw9voWH02lrClt/yw==} - '@next/swc-darwin-arm64@15.2.3': - resolution: {integrity: sha512-uaBhA8aLbXLqwjnsHSkxs353WrRgQgiFjduDpc7YXEU0B54IKx3vU+cxQlYwPCyC8uYEEX7THhtQQsfHnvv8dw==} + '@next/swc-darwin-arm64@15.2.4': + resolution: {integrity: sha512-1AnMfs655ipJEDC/FHkSr0r3lXBgpqKo4K1kiwfUf3iE68rDFXZ1TtHdMvf7D0hMItgDZ7Vuq3JgNMbt/+3bYw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.2.3': - resolution: {integrity: sha512-pVwKvJ4Zk7h+4hwhqOUuMx7Ib02u3gDX3HXPKIShBi9JlYllI0nU6TWLbPT94dt7FSi6mSBhfc2JrHViwqbOdw==} + '@next/swc-darwin-x64@15.2.4': + resolution: {integrity: sha512-3qK2zb5EwCwxnO2HeO+TRqCubeI/NgCe+kL5dTJlPldV/uwCnUgC7VbEzgmxbfrkbjehL4H9BPztWOEtsoMwew==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.2.3': - resolution: {integrity: sha512-50ibWdn2RuFFkOEUmo9NCcQbbV9ViQOrUfG48zHBCONciHjaUKtHcYFiCwBVuzD08fzvzkWuuZkd4AqbvKO7UQ==} + '@next/swc-linux-arm64-gnu@15.2.4': + resolution: {integrity: sha512-HFN6GKUcrTWvem8AZN7tT95zPb0GUGv9v0d0iyuTb303vbXkkbHDp/DxufB04jNVD+IN9yHy7y/6Mqq0h0YVaQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.2.3': - resolution: {integrity: sha512-2gAPA7P652D3HzR4cLyAuVYwYqjG0mt/3pHSWTCyKZq/N/dJcUAEoNQMyUmwTZWCJRKofB+JPuDVP2aD8w2J6Q==} + '@next/swc-linux-arm64-musl@15.2.4': + resolution: {integrity: sha512-Oioa0SORWLwi35/kVB8aCk5Uq+5/ZIumMK1kJV+jSdazFm2NzPDztsefzdmzzpx5oGCJ6FkUC7vkaUseNTStNA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.2.3': - resolution: {integrity: sha512-ODSKvrdMgAJOVU4qElflYy1KSZRM3M45JVbeZu42TINCMG3anp7YCBn80RkISV6bhzKwcUqLBAmOiWkaGtBA9w==} + '@next/swc-linux-x64-gnu@15.2.4': + resolution: {integrity: sha512-yb5WTRaHdkgOqFOZiu6rHV1fAEK0flVpaIN2HB6kxHVSy/dIajWbThS7qON3W9/SNOH2JWkVCyulgGYekMePuw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.2.3': - resolution: {integrity: sha512-ZR9kLwCWrlYxwEoytqPi1jhPd1TlsSJWAc+H/CJHmHkf2nD92MQpSRIURR1iNgA/kuFSdxB8xIPt4p/T78kwsg==} + '@next/swc-linux-x64-musl@15.2.4': + resolution: {integrity: sha512-Dcdv/ix6srhkM25fgXiyOieFUkz+fOYkHlydWCtB0xMST6X9XYI3yPDKBZt1xuhOytONsIFJFB08xXYsxUwJLw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.2.3': - resolution: {integrity: sha512-+G2FrDcfm2YDbhDiObDU/qPriWeiz/9cRR0yMWJeTLGGX6/x8oryO3tt7HhodA1vZ8r2ddJPCjtLcpaVl7TE2Q==} + '@next/swc-win32-arm64-msvc@15.2.4': + resolution: {integrity: sha512-dW0i7eukvDxtIhCYkMrZNQfNicPDExt2jPb9AZPpL7cfyUo7QSNl1DjsHjmmKp6qNAqUESyT8YFl/Aw91cNJJg==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.2.3': - resolution: {integrity: sha512-gHYS9tc+G2W0ZC8rBL+H6RdtXIyk40uLiaos0yj5US85FNhbFEndMA2nW3z47nzOWiSvXTZ5kBClc3rD0zJg0w==} + '@next/swc-win32-x64-msvc@15.2.4': + resolution: {integrity: sha512-SbnWkJmkS7Xl3kre8SdMF6F/XDh1DTFEhp0jRTj/uB8iPKoU2bb2NDfcu+iifv1+mxQEd1g2vvSxcZbXSKyWiQ==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -3148,8 +3148,8 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@15.2.3: - resolution: {integrity: sha512-x6eDkZxk2rPpu46E1ZVUWIBhYCLszmUY6fvHBFcbzJ9dD+qRX6vcHusaqqDlnY+VngKzKbAiG2iRCkPbmi8f7w==} + next@15.2.4: + resolution: {integrity: sha512-VwL+LAaPSxEkd3lU2xWbgEOtrM8oedmyhBqaVNmgKB+GvZlCy9rgaEc+y2on0wv+l0oSFqLtYD6dcC1eAedUaQ==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: @@ -4523,34 +4523,34 @@ snapshots: '@tybys/wasm-util': 0.9.0 optional: true - '@next/env@15.2.3': {} + '@next/env@15.2.4': {} '@next/eslint-plugin-next@15.1.6': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@15.2.3': + '@next/swc-darwin-arm64@15.2.4': optional: true - '@next/swc-darwin-x64@15.2.3': + '@next/swc-darwin-x64@15.2.4': optional: true - '@next/swc-linux-arm64-gnu@15.2.3': + '@next/swc-linux-arm64-gnu@15.2.4': optional: true - '@next/swc-linux-arm64-musl@15.2.3': + '@next/swc-linux-arm64-musl@15.2.4': optional: true - '@next/swc-linux-x64-gnu@15.2.3': + '@next/swc-linux-x64-gnu@15.2.4': optional: true - '@next/swc-linux-x64-musl@15.2.3': + '@next/swc-linux-x64-musl@15.2.4': optional: true - '@next/swc-win32-arm64-msvc@15.2.3': + '@next/swc-win32-arm64-msvc@15.2.4': optional: true - '@next/swc-win32-x64-msvc@15.2.3': + '@next/swc-win32-x64-msvc@15.2.4': optional: true '@nodelib/fs.scandir@2.1.5': @@ -5477,7 +5477,7 @@ snapshots: '@sentry/core@9.9.0': {} - '@sentry/nextjs@9.9.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.2.3(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(webpack@5.98.0)': + '@sentry/nextjs@9.9.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(webpack@5.98.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.30.0 @@ -5490,7 +5490,7 @@ snapshots: '@sentry/vercel-edge': 9.9.0 '@sentry/webpack-plugin': 3.2.2(webpack@5.98.0) chalk: 3.0.0 - next: 15.2.3(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) resolve: 1.22.8 rollup: 4.35.0 stacktrace-parser: 0.1.11 @@ -6224,9 +6224,9 @@ snapshots: '@unrs/rspack-resolver-binding-win32-x64-msvc@1.2.2': optional: true - '@vercel/analytics@1.5.0(next@15.2.3(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)': + '@vercel/analytics@1.5.0(next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)': optionalDependencies: - next: 15.2.3(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: 19.0.0 '@webassemblyjs/ast@1.14.1': @@ -7657,9 +7657,9 @@ snapshots: neotraverse@0.6.18: {} - next-runtime-env@3.2.2(next@15.2.3(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0): + next-runtime-env@3.2.2(next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0): dependencies: - next: 15.2.3(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: 19.0.0 next-themes@0.4.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0): @@ -7667,9 +7667,9 @@ snapshots: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) - next@15.2.3(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: - '@next/env': 15.2.3 + '@next/env': 15.2.4 '@swc/counter': 0.1.3 '@swc/helpers': 0.5.15 busboy: 1.6.0 @@ -7679,23 +7679,23 @@ snapshots: react-dom: 19.0.0(react@19.0.0) styled-jsx: 5.1.6(@babel/core@7.26.10)(react@19.0.0) optionalDependencies: - '@next/swc-darwin-arm64': 15.2.3 - '@next/swc-darwin-x64': 15.2.3 - '@next/swc-linux-arm64-gnu': 15.2.3 - '@next/swc-linux-arm64-musl': 15.2.3 - '@next/swc-linux-x64-gnu': 15.2.3 - '@next/swc-linux-x64-musl': 15.2.3 - '@next/swc-win32-arm64-msvc': 15.2.3 - '@next/swc-win32-x64-msvc': 15.2.3 + '@next/swc-darwin-arm64': 15.2.4 + '@next/swc-darwin-x64': 15.2.4 + '@next/swc-linux-arm64-gnu': 15.2.4 + '@next/swc-linux-arm64-musl': 15.2.4 + '@next/swc-linux-x64-gnu': 15.2.4 + '@next/swc-linux-x64-musl': 15.2.4 + '@next/swc-win32-arm64-msvc': 15.2.4 + '@next/swc-win32-x64-msvc': 15.2.4 '@opentelemetry/api': 1.9.0 sharp: 0.33.5 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros - nextjs-toploader@3.8.15(next@15.2.3(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + nextjs-toploader@3.8.15(next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: - next: 15.2.3(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) nprogress: 0.2.0 prop-types: 15.8.1 react: 19.0.0 diff --git a/packages/dashboard/src/app/(layout)/components/OnboardingModal.tsx b/packages/dashboard/src/app/(layout)/components/OnboardingModal.tsx index a2551a8..cd358e4 100644 --- a/packages/dashboard/src/app/(layout)/components/OnboardingModal.tsx +++ b/packages/dashboard/src/app/(layout)/components/OnboardingModal.tsx @@ -11,21 +11,46 @@ import {createClient} from "@/utils/supabase/client" import {parseSwagger} from "@/app/(layout)/services/import/parseSwagger" import {demoSwagger} from "@/utils/data/demoSwagger" +const completeOnboarding = () => { + localStorage.setItem('onboarding_complete', 'true') +} + export default function OnboardingModal() { const router = useRouter() const supabase = createClient() const [isOpen, setIsOpen] = useState(false) const [isLoading, setIsLoading] = useState(false) - // Show the modal when the component mounts (for new users) useEffect(() => { - // In a real app, you'd check if the user is new before showing the modal - // For example: if (localStorage.getItem('isNewUser') === 'true') - setIsOpen(true) + const checkUserServices = async () => { + try { + const onboardingComplete = localStorage.getItem('onboarding_complete') === 'true' + if (onboardingComplete) { + return + } + + const {data: services, error} = await supabase + .from('services') + .select('id') + .limit(1) + + if (error) { + console.error("Error checking user services:", error) + return + } + + if (!services || services.length === 0) { + setIsOpen(true) + } else { + completeOnboarding() + } + } catch (error) { + console.error("Error in onboarding check:", error) + } + } - // You could set a flag to not show this again - // localStorage.setItem('isNewUser', 'false') - }, []) + checkUserServices() + }, [supabase]) const onDemoProject = async () => { try { @@ -57,6 +82,7 @@ export default function OnboardingModal() { if (endpointsError) { throw endpointsError } + completeOnboarding() setIsOpen(false) toast.success("Demo project created successfully!") router.push(`/services/${serviceData.id}`) @@ -72,15 +98,22 @@ export default function OnboardingModal() { } const onImport = () => { + completeOnboarding() router.push("/services/import") setIsOpen(false) } const onManual = () => { + completeOnboarding() router.push("/services/new") setIsOpen(false) } + const onExploreOwn = () => { + completeOnboarding() + setIsOpen(false) + } + return ( @@ -176,7 +209,7 @@ export default function OnboardingModal() { +
+ + + +
+ + + + Use as API + + + + + Get an API key to start making requests directly from your app or backend. + + +
+
+ +
+
+ +
+ +
+ + + ) +} diff --git a/packages/dashboard/src/app/(layout)/services/[id]/page.tsx b/packages/dashboard/src/app/(layout)/services/[id]/page.tsx index dee6942..d07986e 100644 --- a/packages/dashboard/src/app/(layout)/services/[id]/page.tsx +++ b/packages/dashboard/src/app/(layout)/services/[id]/page.tsx @@ -14,6 +14,7 @@ import {EndpointsTab} from "./components/EndpointsTab"; import {APIServiceForm} from "@/components/forms/APIServiceForm"; import DeleteServiceForm from "@/components/forms/DeleteServiceForm"; import ServiceMonitoring from "@/app/(layout)/services/[id]/components/monitoring/ServiceMonitoring"; +import ServiceOnboardingModal from "@/app/(layout)/services/[id]/components/ServiceOnboardingModal"; type Args = { params: Promise<{ id: string }> @@ -44,6 +45,7 @@ export default async function PrivatePage({params}: Args) { return (
+ From 94dce1750181921f60a59345dd63a3da7d397bb9 Mon Sep 17 00:00:00 2001 From: "maxim.budnik" Date: Sun, 20 Apr 2025 22:37:17 +0300 Subject: [PATCH 5/6] onboarding for service screen with suggestion to grab creds --- .../services/[id]/components/ServiceOnboardingModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dashboard/src/app/(layout)/services/[id]/components/ServiceOnboardingModal.tsx b/packages/dashboard/src/app/(layout)/services/[id]/components/ServiceOnboardingModal.tsx index f10c93e..b0dab1b 100644 --- a/packages/dashboard/src/app/(layout)/services/[id]/components/ServiceOnboardingModal.tsx +++ b/packages/dashboard/src/app/(layout)/services/[id]/components/ServiceOnboardingModal.tsx @@ -97,7 +97,7 @@ export default function ServiceOnboardingModal() { onClick={completeModal} className="text-muted-foreground" > - I'll get API key & config later + Skip for now - I’ll set it up later
From bfbd75f9c37df8a80085af2373d0ace8ed5c60eb Mon Sep 17 00:00:00 2001 From: "maxim.budnik" Date: Sun, 20 Apr 2025 22:44:11 +0300 Subject: [PATCH 6/6] typo fix --- .../dashboard/src/app/(layout)/components/OnboardingModal.tsx | 2 +- .../services/[id]/components/ServiceOnboardingModal.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dashboard/src/app/(layout)/components/OnboardingModal.tsx b/packages/dashboard/src/app/(layout)/components/OnboardingModal.tsx index f547309..ace8767 100644 --- a/packages/dashboard/src/app/(layout)/components/OnboardingModal.tsx +++ b/packages/dashboard/src/app/(layout)/components/OnboardingModal.tsx @@ -213,7 +213,7 @@ export default function OnboardingModal() { className="text-muted-foreground" disabled={isLoading} > - I'll explore on my own + I'll explore on my own diff --git a/packages/dashboard/src/app/(layout)/services/[id]/components/ServiceOnboardingModal.tsx b/packages/dashboard/src/app/(layout)/services/[id]/components/ServiceOnboardingModal.tsx index b0dab1b..ace670f 100644 --- a/packages/dashboard/src/app/(layout)/services/[id]/components/ServiceOnboardingModal.tsx +++ b/packages/dashboard/src/app/(layout)/services/[id]/components/ServiceOnboardingModal.tsx @@ -97,7 +97,7 @@ export default function ServiceOnboardingModal() { onClick={completeModal} className="text-muted-foreground" > - Skip for now - I’ll set it up later + Skip for now - I'll set it up later