feat(www): interactive DB viewer hero + max-w-7xl centering (#28)
Replace the static hero screenshot with a self-contained React mockup of the full Gridline DB viewer (sidebar, table tree, tab bar, fake SQL editor, data grid, queries/objects/schema-visualizer views, disabled Tools state). - Add @astrojs/react and mount the island via client:visible - 16:9 aspect-ratio mockup, hidden on mobile (static image fallback) - JSON cell popover: solid surface, anchored within the mockup window, Escape/outside-click close - Dependency-free HTML5 drag-to-reorder tabs - Theme-aware app tokens (canvas, surface-raised, popover bg) - Site-wide max-w-7xl (was max-w-[1200px]) so sections center on big screens - Regenerate standalone www/bun.lock for deterministic Dokploy builds
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,302 @@
|
||||
// Hardcoded sample data for the interactive DB viewer hero mockup.
|
||||
// No real database — just realistic-looking rows so the grid feels alive.
|
||||
|
||||
export interface DemoColumn {
|
||||
name: string;
|
||||
data_type: string;
|
||||
is_pk?: boolean;
|
||||
is_fk?: boolean;
|
||||
fk_ref?: [string, string];
|
||||
is_nullable?: boolean;
|
||||
}
|
||||
|
||||
export interface DemoTable {
|
||||
name: string;
|
||||
table_type: "TABLE" | "VIEW";
|
||||
columns: DemoColumn[];
|
||||
rows: unknown[][];
|
||||
}
|
||||
|
||||
export const demoTables: DemoTable[] = [
|
||||
{
|
||||
name: "users",
|
||||
table_type: "TABLE",
|
||||
columns: [
|
||||
{ name: "id", data_type: "int4", is_pk: true },
|
||||
{ name: "name", data_type: "text" },
|
||||
{ name: "email", data_type: "text" },
|
||||
{ name: "role", data_type: "text" },
|
||||
{ name: "created_at", data_type: "timestamptz" },
|
||||
{ name: "metadata", data_type: "jsonb" },
|
||||
],
|
||||
rows: [
|
||||
[1, "Ada Lovelace", "ada@example.com", "admin", "2024-01-15 09:30:00+00", { active: true, plan: "pro" }],
|
||||
[2, "Grace Hopper", "grace@example.com", "editor", "2024-02-03 14:12:00+00", { active: true, plan: "free" }],
|
||||
[3, "Alan Turing", "alan@example.com", "viewer", "2024-03-22 08:45:00+00", { active: false, plan: "free" }],
|
||||
[4, "Katherine Johnson", "katherine@example.com", "editor", "2024-04-10 18:20:00+00", { active: true, plan: "pro" }],
|
||||
[5, "Margaret Hamilton", "margaret@example.com", "admin", "2024-05-01 11:05:00+00", { active: true, plan: "enterprise" }],
|
||||
[6, "Tim Berners-Lee", "tim@example.com", "viewer", "2024-06-19 16:40:00+00", { active: true, plan: "free" }],
|
||||
[7, "Barbara Liskov", "barbara@example.com", "editor", "2024-07-07 10:15:00+00", { active: false, plan: "pro" }],
|
||||
[8, "Donald Knuth", "donald@example.com", "viewer", "2024-08-25 13:55:00+00", { active: true, plan: "free" }],
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "orders",
|
||||
table_type: "TABLE",
|
||||
columns: [
|
||||
{ name: "id", data_type: "int4", is_pk: true },
|
||||
{ name: "user_id", data_type: "int4", is_fk: true, fk_ref: ["users", "id"] },
|
||||
{ name: "total", data_type: "numeric" },
|
||||
{ name: "status", data_type: "text" },
|
||||
{ name: "created_at", data_type: "timestamptz" },
|
||||
],
|
||||
rows: [
|
||||
[1, 1, 129.99, "completed", "2024-05-12 10:00:00+00"],
|
||||
[2, 2, 45.5, "pending", "2024-06-01 09:30:00+00"],
|
||||
[3, 1, 89.0, "shipped", "2024-06-15 14:20:00+00"],
|
||||
[4, 4, 250.0, "completed", "2024-07-02 11:45:00+00"],
|
||||
[5, 6, 19.99, "cancelled", "2024-07-20 08:10:00+00"],
|
||||
[6, 2, 320.75, "completed", "2024-08-08 17:05:00+00"],
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "products",
|
||||
table_type: "TABLE",
|
||||
columns: [
|
||||
{ name: "id", data_type: "int4", is_pk: true },
|
||||
{ name: "name", data_type: "text" },
|
||||
{ name: "price", data_type: "numeric" },
|
||||
{ name: "stock", data_type: "int4" },
|
||||
{ name: "category_id", data_type: "int4", is_fk: true, fk_ref: ["categories", "id"] },
|
||||
],
|
||||
rows: [
|
||||
[1, "Wireless Mouse", 29.99, 150, 1],
|
||||
[2, "Mechanical Keyboard", 89.0, 42, 1],
|
||||
[3, '27" Monitor', 249.99, 18, 3],
|
||||
[4, "USB-C Hub", 45.5, 200, 1],
|
||||
[5, "Laptop Stand", 39.0, 75, 2],
|
||||
[6, "Desk Mat", 24.99, 120, 2],
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "categories",
|
||||
table_type: "TABLE",
|
||||
columns: [
|
||||
{ name: "id", data_type: "int4", is_pk: true },
|
||||
{ name: "name", data_type: "text" },
|
||||
],
|
||||
rows: [
|
||||
[1, "Accessories"],
|
||||
[2, "Furniture"],
|
||||
[3, "Electronics"],
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "audit_log",
|
||||
table_type: "TABLE",
|
||||
columns: [
|
||||
{ name: "id", data_type: "int4", is_pk: true },
|
||||
{ name: "action", data_type: "text" },
|
||||
{ name: "details", data_type: "jsonb" },
|
||||
{ name: "created_at", data_type: "timestamptz" },
|
||||
],
|
||||
rows: [
|
||||
[1, "user.login", { ip: "192.168.1.10", device: "macos" }, "2024-08-01 09:00:00+00"],
|
||||
[2, "order.created", { order_id: 6, total: 320.75 }, "2024-08-08 17:05:00+00"],
|
||||
[3, "user.updated", { user_id: 3, field: "role" }, "2024-08-10 12:30:00+00"],
|
||||
[4, "export.run", { format: "csv", rows: 500 }, "2024-08-12 15:45:00+00"],
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "order_summary",
|
||||
table_type: "VIEW",
|
||||
columns: [
|
||||
{ name: "user_id", data_type: "int4" },
|
||||
{ name: "total_orders", data_type: "int8" },
|
||||
{ name: "total_spent", data_type: "numeric" },
|
||||
],
|
||||
rows: [
|
||||
[1, 2, 218.99],
|
||||
[2, 2, 366.25],
|
||||
[4, 1, 250.0],
|
||||
[6, 1, 19.99],
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Canned result shown when the user hits "Run Query" in the mockup.
|
||||
export const cannedQueryResult: { columns: DemoColumn[]; rows: unknown[][] } = {
|
||||
columns: [
|
||||
{ name: "id", data_type: "int4", is_pk: true },
|
||||
{ name: "name", data_type: "text" },
|
||||
{ name: "email", data_type: "text" },
|
||||
{ name: "role", data_type: "text" },
|
||||
{ name: "created_at", data_type: "timestamptz" },
|
||||
],
|
||||
rows: [
|
||||
[1, "Ada Lovelace", "ada@example.com", "admin", "2024-01-15 09:30:00+00"],
|
||||
[2, "Grace Hopper", "grace@example.com", "editor", "2024-02-03 14:12:00+00"],
|
||||
[3, "Alan Turing", "alan@example.com", "viewer", "2024-03-22 08:45:00+00"],
|
||||
[4, "Katherine Johnson", "katherine@example.com", "editor", "2024-04-10 18:20:00+00"],
|
||||
[5, "Margaret Hamilton", "margaret@example.com", "admin", "2024-05-01 11:05:00+00"],
|
||||
],
|
||||
};
|
||||
|
||||
export const defaultQuery = `SELECT id, name, email, role, created_at
|
||||
FROM users
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 5;`;
|
||||
|
||||
export function abbreviateType(t: string): string {
|
||||
const map: Record<string, string> = {
|
||||
int4: "int4",
|
||||
int8: "int8",
|
||||
text: "text",
|
||||
numeric: "num",
|
||||
timestamptz: "ts",
|
||||
jsonb: "jsonb",
|
||||
};
|
||||
return map[t] ?? t;
|
||||
}
|
||||
|
||||
// ─── Queries history (Queries view) ─────────────────────
|
||||
|
||||
export interface DemoQuery {
|
||||
sql: string;
|
||||
ms: number;
|
||||
rows: number;
|
||||
favorite?: boolean;
|
||||
}
|
||||
|
||||
export const demoQueries: DemoQuery[] = [
|
||||
{ sql: "SELECT * FROM users WHERE role = 'admin';", ms: 12, rows: 2, favorite: true },
|
||||
{ sql: "SELECT id, name, price FROM products ORDER BY price DESC;", ms: 8, rows: 6 },
|
||||
{ sql: "SELECT status, COUNT(*) FROM orders GROUP BY status;", ms: 15, rows: 4 },
|
||||
{ sql: "SELECT u.name, o.total FROM users u JOIN orders o ON o.user_id = u.id;", ms: 21, rows: 6 },
|
||||
{ sql: "SELECT * FROM audit_log ORDER BY created_at DESC LIMIT 10;", ms: 9, rows: 4, favorite: true },
|
||||
{ sql: "VACUUM ANALYZE;", ms: 340, rows: 0 },
|
||||
];
|
||||
|
||||
// ─── Objects (Objects view) ─────────────────────────────
|
||||
|
||||
export interface DemoObjectItem {
|
||||
name: string;
|
||||
schema?: string;
|
||||
signature?: string;
|
||||
detail: { label: string; value: string; mono?: boolean; accent?: boolean }[];
|
||||
source?: string;
|
||||
labels?: string[];
|
||||
}
|
||||
|
||||
export type ObjectType = "functions" | "triggers" | "sequences" | "enums" | "extensions";
|
||||
|
||||
export const demoObjects: Record<ObjectType, DemoObjectItem[]> = {
|
||||
functions: [
|
||||
{
|
||||
name: "get_user_orders",
|
||||
schema: "public",
|
||||
signature: "(uid int4)",
|
||||
detail: [
|
||||
{ label: "Returns", value: "SETOF orders", mono: true, accent: true },
|
||||
{ label: "Language", value: "plpgsql" },
|
||||
{ label: "Kind", value: "Function" },
|
||||
{ label: "Schema", value: "public", mono: true },
|
||||
],
|
||||
source: `CREATE OR REPLACE FUNCTION public.get_user_orders(uid int4)
|
||||
RETURNS SETOF orders
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT *
|
||||
FROM orders
|
||||
WHERE user_id = uid
|
||||
ORDER BY created_at DESC;
|
||||
END;
|
||||
$function$;`,
|
||||
},
|
||||
{
|
||||
name: "total_spent",
|
||||
schema: "public",
|
||||
signature: "(uid int4)",
|
||||
detail: [
|
||||
{ label: "Returns", value: "numeric", mono: true, accent: true },
|
||||
{ label: "Language", value: "sql" },
|
||||
{ label: "Kind", value: "Function" },
|
||||
{ label: "Schema", value: "public", mono: true },
|
||||
],
|
||||
source: `CREATE FUNCTION public.total_spent(uid int4)
|
||||
RETURNS numeric
|
||||
LANGUAGE sql
|
||||
AS $function$
|
||||
SELECT COALESCE(SUM(total), 0)
|
||||
FROM orders
|
||||
WHERE user_id = uid
|
||||
AND status <> 'cancelled';
|
||||
$function$;`,
|
||||
},
|
||||
],
|
||||
triggers: [
|
||||
{
|
||||
name: "set_updated_at",
|
||||
schema: "public",
|
||||
detail: [
|
||||
{ label: "Table", value: "users", mono: true },
|
||||
{ label: "Event", value: "UPDATE" },
|
||||
{ label: "Timing", value: "BEFORE" },
|
||||
{ label: "Orientation", value: "ROW" },
|
||||
{ label: "Status", value: "ENABLED" },
|
||||
],
|
||||
source: `CREATE TRIGGER set_updated_at
|
||||
BEFORE UPDATE ON users
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();`,
|
||||
},
|
||||
],
|
||||
sequences: [
|
||||
{ name: "users_id_seq", schema: "public", signature: "users.id", detail: [
|
||||
{ label: "Current value", value: "8", mono: true, accent: true },
|
||||
{ label: "Increment", value: "1", mono: true },
|
||||
{ label: "Start", value: "1", mono: true },
|
||||
{ label: "Min", value: "1", mono: true },
|
||||
{ label: "Max", value: "9223372036854775807", mono: true },
|
||||
{ label: "Cycle", value: "No" },
|
||||
] },
|
||||
{ name: "orders_id_seq", schema: "public", signature: "orders.id", detail: [
|
||||
{ label: "Current value", value: "6", mono: true, accent: true },
|
||||
{ label: "Increment", value: "1", mono: true },
|
||||
{ label: "Start", value: "1", mono: true },
|
||||
{ label: "Min", value: "1", mono: true },
|
||||
{ label: "Max", value: "9223372036854775807", mono: true },
|
||||
{ label: "Cycle", value: "No" },
|
||||
] },
|
||||
],
|
||||
enums: [
|
||||
{
|
||||
name: "user_role",
|
||||
schema: "public",
|
||||
labels: ["admin", "editor", "viewer"],
|
||||
detail: [],
|
||||
},
|
||||
],
|
||||
extensions: [
|
||||
{ name: "pgcrypto", schema: "public", signature: "1.3", detail: [
|
||||
{ label: "Version", value: "1.3", mono: true, accent: true },
|
||||
{ label: "Schema", value: "public", mono: true },
|
||||
{ label: "Comment", value: "cryptographic functions" },
|
||||
] },
|
||||
{ name: "pg_stat_statements", schema: "public", signature: "1.10", detail: [
|
||||
{ label: "Version", value: "1.10", mono: true, accent: true },
|
||||
{ label: "Schema", value: "public", mono: true },
|
||||
{ label: "Comment", value: "track execution statistics of all SQL statements" },
|
||||
] },
|
||||
],
|
||||
};
|
||||
|
||||
export const objectTypeLabels: Record<ObjectType, string> = {
|
||||
functions: "Functions",
|
||||
triggers: "Triggers",
|
||||
sequences: "Sequences",
|
||||
enums: "Enums",
|
||||
extensions: "Extensions",
|
||||
};
|
||||
@@ -91,7 +91,7 @@ const isProd = import.meta.env.PROD;
|
||||
</head>
|
||||
<body>
|
||||
<nav id="nav" class="fixed top-0 inset-x-0 z-50 transition-all duration-300 border-b border-transparent">
|
||||
<div class="max-w-[1200px] mx-auto px-6 py-4 flex items-center justify-between">
|
||||
<div class="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
|
||||
<a href="/" class="flex items-center gap-2.5 group">
|
||||
<img src="/gridline-icon.svg" alt="Gridline" class="w-7 h-7" />
|
||||
<span class="text-lg font-semibold tracking-tight">Gridline</span>
|
||||
@@ -111,7 +111,7 @@ const isProd = import.meta.env.PROD;
|
||||
<slot />
|
||||
|
||||
<footer class="border-t border-[var(--border)] mt-20 md:mt-28">
|
||||
<div class="max-w-[1200px] mx-auto px-6 py-16 md:py-20">
|
||||
<div class="max-w-7xl mx-auto px-6 py-16 md:py-20">
|
||||
<div class="grid md:grid-cols-4 gap-10 md:gap-8">
|
||||
<div class="md:col-span-1">
|
||||
<a href="/" class="flex items-center gap-2.5 mb-4">
|
||||
|
||||
+20
-14
@@ -8,6 +8,7 @@ import {
|
||||
buildWebSiteJsonLd,
|
||||
} from "../lib/seo";
|
||||
import { Image } from "astro:assets";
|
||||
import HeroDbViewer from "../components/hero/HeroDbViewer";
|
||||
import queryEditor from "../assets/screenshots/query-editor.png";
|
||||
import erDiagram from "../assets/screenshots/er-diagram.png";
|
||||
import home from "../assets/screenshots/home.png";
|
||||
@@ -48,7 +49,7 @@ const jsonLd = [
|
||||
|
||||
<!-- Hero -->
|
||||
<header id="hero" class="relative pt-32 pb-20 md:pt-44 md:pb-28 overflow-hidden">
|
||||
<div class="max-w-[1200px] mx-auto px-6 text-center">
|
||||
<div class="max-w-7xl mx-auto px-6 text-center">
|
||||
<div class="hero-animate">
|
||||
<p class="font-mono text-xs tracking-[0.15em] uppercase text-[var(--accent)] mb-5">Open-Source Database GUI</p>
|
||||
<h1 class="font-serif text-5xl md:text-7xl lg:text-8xl leading-[1.05] tracking-tight max-w-4xl mx-auto lowercase">
|
||||
@@ -75,17 +76,22 @@ const jsonLd = [
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-screenshot mt-14 md:mt-20 mx-auto max-w-5xl">
|
||||
<Image
|
||||
src={queryEditor}
|
||||
alt="Gridline query editor and data grid"
|
||||
widths={[640, 1024, 1600]}
|
||||
sizes="(max-width: 1024px) 100vw, 1024px"
|
||||
formats={["webp", "avif"]}
|
||||
loading="eager"
|
||||
fetchpriority="high"
|
||||
class="w-full h-auto"
|
||||
/>
|
||||
<div class="hero-screenshot mt-14 md:mt-20 mx-auto max-w-6xl">
|
||||
<div class="hidden md:block aspect-[16/9]">
|
||||
<HeroDbViewer client:visible />
|
||||
</div>
|
||||
<div class="md:hidden">
|
||||
<Image
|
||||
src={queryEditor}
|
||||
alt="Gridline query editor and data grid"
|
||||
widths={[640, 1024, 1600]}
|
||||
sizes="(max-width: 1024px) 100vw, 1024px"
|
||||
formats={["webp", "avif"]}
|
||||
loading="eager"
|
||||
fetchpriority="high"
|
||||
class="w-full h-auto"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-8 text-xs md:text-sm text-[var(--text-muted)] font-mono">
|
||||
@@ -96,7 +102,7 @@ const jsonLd = [
|
||||
|
||||
<!-- Why Gridline -->
|
||||
<section id="why" class="section-animate py-20 md:py-28">
|
||||
<div class="max-w-[1200px] mx-auto px-6">
|
||||
<div class="max-w-7xl mx-auto px-6">
|
||||
<h2 class="font-serif text-3xl md:text-5xl text-center max-w-2xl mx-auto">
|
||||
Everything the paid apps lock behind a subscription.
|
||||
</h2>
|
||||
@@ -128,7 +134,7 @@ const jsonLd = [
|
||||
|
||||
<!-- Feature highlights -->
|
||||
<section id="features" class="section-animate py-20 md:py-28">
|
||||
<div class="max-w-[1200px] mx-auto px-6">
|
||||
<div class="max-w-7xl mx-auto px-6">
|
||||
<p class="font-mono text-xs text-[var(--accent)] uppercase tracking-[0.15em] text-center mb-3">Features</p>
|
||||
<h2 class="font-serif text-3xl md:text-5xl text-center mb-14">Explore, query, and move data.</h2>
|
||||
<div class="grid md:grid-cols-2 gap-5">
|
||||
|
||||
@@ -18,6 +18,16 @@
|
||||
--primary: #3b82f6;
|
||||
--primary-hover: #60a5fa;
|
||||
--accent: #f59e0b;
|
||||
|
||||
/* App-style tokens (mirror the desktop DB viewer) */
|
||||
--canvas: #0a0a0a;
|
||||
--surface-raised: rgba(255, 255, 255, 0.07);
|
||||
--text-subtle: rgba(255, 255, 255, 0.35);
|
||||
--accent-hover: #fbbf24;
|
||||
|
||||
/* Popover/dropdown surfaces (solid, readable — mirror the desktop app) */
|
||||
--popover-bg: #18181b;
|
||||
--popover-border: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
html.light {
|
||||
@@ -30,6 +40,16 @@ html.light {
|
||||
--primary: #2563eb;
|
||||
--primary-hover: #1d4ed8;
|
||||
--accent: #d97706;
|
||||
|
||||
/* App-style tokens (mirror the desktop DB viewer) */
|
||||
--canvas: #fafafa;
|
||||
--surface-raised: rgba(0, 0, 0, 0.06);
|
||||
--text-subtle: rgba(0, 0, 0, 0.35);
|
||||
--accent-hover: #b45309;
|
||||
|
||||
/* Popover/dropdown surfaces (solid, readable — mirror the desktop app) */
|
||||
--popover-bg: #ffffff;
|
||||
--popover-border: rgba(0, 0, 0, 0.10);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Reference in New Issue
Block a user