From ace62632c9b09dc56d2ecab640e10b9e9faf23ad Mon Sep 17 00:00:00 2001 From: Krishna Santosh <75202541+krishna-santosh@users.noreply.github.com> Date: Sat, 20 Jun 2026 18:38:17 +0530 Subject: [PATCH 01/43] feat(dashboard): update file picker dialog --- .../src/components/file-picker-dialog.tsx | 187 ++++-------------- 1 file changed, 39 insertions(+), 148 deletions(-) diff --git a/apps/dashboard/src/components/file-picker-dialog.tsx b/apps/dashboard/src/components/file-picker-dialog.tsx index 0c02b48e..0a907893 100644 --- a/apps/dashboard/src/components/file-picker-dialog.tsx +++ b/apps/dashboard/src/components/file-picker-dialog.tsx @@ -5,22 +5,12 @@ import { ImagePlus, Music, Search, - Trash2, Upload, Video, } from "lucide-react"; import { useCallback, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog"; + import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { @@ -32,13 +22,11 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; +import { ScrollArea } from "@/components/ui/scroll-area"; import { Skeleton } from "@/components/ui/skeleton"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { - deleteFile, type FileItem, - type FileReference, - getFileReferences, getFiles, uploadFile, } from "@/lib/api"; @@ -139,16 +127,13 @@ export function FilePickerDialog({ const [page, setPage] = useState(1); const [tab, setTab] = useState("library"); const [dragOver, setDragOver] = useState(false); - const [pendingDelete, setPendingDelete] = useState<{ - file: FileItem; - refs: FileReference[]; - } | null>(null); const fileInputRef = useRef(null); const queryClient = useQueryClient(); // Derived values – memoised so they don't recalculate on every render. const fileType = useMemo(() => deriveFileType(accept), [accept]); + const skeletonKeys = useMemo(() => Array.from({ length: 8 }, (_, i) => i), []); const { data, isLoading, isError } = useQuery({ queryKey: ["files", siteId, page, search, fileType], @@ -194,16 +179,6 @@ export function FilePickerDialog({ onError: (err: Error) => toast.error(err.message), }); - const deleteMutation = useMutation({ - mutationFn: (fileId: string) => deleteFile(siteId, fileId), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["files", siteId] }); - toast.success("File deleted"); - setPendingDelete(null); - }, - onError: (err: Error) => toast.error(err.message), - }); - // --------------------------------------------------------------------------- // Handlers // --------------------------------------------------------------------------- @@ -226,33 +201,6 @@ export function FilePickerDialog({ [handleFileSelect], ); - const handleDelete = useCallback( - async (file: FileItem) => { - let refs: FileReference[] = []; - try { - refs = await getFileReferences(siteId, file.id); - } catch { - // Treat a failed reference lookup the same as "no references" so the - // user can still delete the file, but warn them just in case. - toast.warning( - "Could not check file references. Proceeding with delete.", - ); - } - - if (refs.length > 0) { - setPendingDelete({ file, refs }); - } else { - deleteMutation.mutate(file.id); - } - }, - [siteId, deleteMutation], - ); - - const confirmDelete = useCallback(() => { - if (!pendingDelete) return; - deleteMutation.mutate(pendingDelete.file.id); - }, [pendingDelete, deleteMutation]); - const handleSearchChange = useCallback( (e: React.ChangeEvent) => { setSearch(e.target.value); @@ -293,14 +241,9 @@ export function FilePickerDialog({ ); - const pendingCollections = pendingDelete - ? [...new Set(pendingDelete.refs.map((r) => r.collection_name))].join(", ") - : ""; - return ( - <> - - + + File Library @@ -311,7 +254,7 @@ export function FilePickerDialog({ Library @@ -323,7 +266,7 @@ export function FilePickerDialog({ ---------------------------------------------------------------- */}
@@ -337,8 +280,8 @@ export function FilePickerDialog({ {isLoading ? (
- {Array.from({ length: 8 }, (_, i) => ( - + {skeletonKeys.map((k) => ( + ))}
) : isError ? ( @@ -350,19 +293,20 @@ export function FilePickerDialog({ No files found.
) : ( -
- {filteredItems.map((file) => ( - { - onSelect(file); - handleOpenChange(false); - }} - onDelete={() => handleDelete(file)} - /> - ))} -
+ +
+ {filteredItems.map((file) => ( + { + onSelect(file); + handleOpenChange(false); + }} + /> + ))} +
+
)} {data && data.total > data.per_page && ( @@ -397,11 +341,14 @@ export function FilePickerDialog({ receives clicks without needing an imperative ref call, and biome's a11y/noStaticElementInteractions lint is satisfied. ---------------------------------------------------------------- */} - +
-
- - {/* ----------------------------------------------------------------------- - Delete-with-references confirmation dialog - ----------------------------------------------------------------------- */} - { - if (!isOpen) setPendingDelete(null); - }} - > - - - Delete file? - - This file is used in {pendingDelete?.refs.length}{" "} - content item - {pendingDelete?.refs.length === 1 ? "" : "s"} - {pendingCollections ? ` (${pendingCollections})` : ""}. Deleting - it may break those pages. - - - - - Cancel - - - {deleteMutation.isPending ? "Deleting…" : "Delete"} - - - - - +
); } @@ -504,27 +415,23 @@ export function FilePickerDialog({ interface FileGridItemProps { file: FileItem; onSelect: () => void; - onDelete: () => void; } -function FileGridItem({ file, onSelect, onDelete }: FileGridItemProps) { +function FileGridItem({ file, onSelect }: FileGridItemProps) { const isImage = file.mime_type.startsWith("image/"); const isVideo = file.mime_type.startsWith("video/"); const hasPreview = isImage || (isVideo && !!file.thumbnail_url); return ( -
{ - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - onSelect(); - } - }} - > +
+ {/* Overlay button captures card clicks */} + -
- {/* Filename / size overlay */}

{file.original_name}

From 813e31798b32ab1bfdee2bfa021d89d310da88ef Mon Sep 17 00:00:00 2001 From: Krishna Santosh <75202541+krishna-santosh@users.noreply.github.com> Date: Sat, 20 Jun 2026 18:38:47 +0530 Subject: [PATCH 02/43] chore(dashboard): fix lint issues --- .../src/components/app-breadcrumb.tsx | 16 +++++---- .../components/backups/backups-section.tsx | 34 +++++++++---------- .../dashboard/src/components/dynamic-form.tsx | 26 +++++++++----- .../src/components/revisions-panel.tsx | 5 +-- .../src/components/sidebar/app-sidebar.tsx | 2 +- .../src/components/sidebar/site-switcher.tsx | 2 +- .../site-settings/user-combobox.tsx | 1 - apps/dashboard/src/lib/api.ts | 4 +-- apps/dashboard/src/routes/_admin.tsx | 2 +- .../_admin/sites.$siteId/collections.tsx | 2 +- 10 files changed, 51 insertions(+), 43 deletions(-) diff --git a/apps/dashboard/src/components/app-breadcrumb.tsx b/apps/dashboard/src/components/app-breadcrumb.tsx index 2ad8821c..6f1165d6 100644 --- a/apps/dashboard/src/components/app-breadcrumb.tsx +++ b/apps/dashboard/src/components/app-breadcrumb.tsx @@ -70,7 +70,7 @@ function useSiteName(siteId: string | undefined) { const { data: site } = useQuery({ queryKey: ["site", siteId], - queryFn: () => getSite(siteId!), + queryFn: () => getSite(siteId as string), enabled: !!siteId && !siteFromList, }); @@ -80,7 +80,7 @@ function useSiteName(siteId: string | undefined) { function useCollectionName(siteId: string | undefined, collectionSlug: string | undefined) { const { data: collections } = useQuery({ queryKey: ["collections", siteId], - queryFn: () => getCollections(siteId!), + queryFn: () => getCollections(siteId as string), enabled: !!siteId && !!collectionSlug, }); @@ -90,7 +90,7 @@ function useCollectionName(siteId: string | undefined, collectionSlug: string | function useSingletonName(siteId: string | undefined, slug: string | undefined) { const { data: collections } = useQuery({ queryKey: ["collections", siteId], - queryFn: () => getCollections(siteId!), + queryFn: () => getCollections(siteId as string), enabled: !!siteId && !!slug, }); @@ -100,7 +100,7 @@ function useSingletonName(siteId: string | undefined, slug: string | undefined) function useEntrySlugLabel(siteId: string | undefined, entryId: string | undefined) { const { data: entry } = useQuery({ queryKey: ["entry", siteId, entryId], - queryFn: () => getEntryById(siteId!, entryId!), + queryFn: () => getEntryById(siteId as string, entryId as string), enabled: !!siteId && !!entryId, }); @@ -125,6 +125,8 @@ function useBreadcrumbLabels(defs: BreadcrumbDef[], params: Record - {config.crumbs.map((_, i) => ( - + {config.crumbs.map((def, i) => ( + {i > 0 && } @@ -207,7 +209,7 @@ export function AppBreadcrumb() { const href = buildHref(routeId, params, i); return ( - + {i > 0 && } {isLast ? ( diff --git a/apps/dashboard/src/components/backups/backups-section.tsx b/apps/dashboard/src/components/backups/backups-section.tsx index f5cdd38a..c6a20d81 100644 --- a/apps/dashboard/src/components/backups/backups-section.tsx +++ b/apps/dashboard/src/components/backups/backups-section.tsx @@ -253,17 +253,17 @@ export function BackupsSection({ scope }: { scope: BackupScope }) {
- - +
); diff --git a/apps/dashboard/src/components/revisions-panel.tsx b/apps/dashboard/src/components/revisions-panel.tsx index 0a657044..e499cefa 100644 --- a/apps/dashboard/src/components/revisions-panel.tsx +++ b/apps/dashboard/src/components/revisions-panel.tsx @@ -390,10 +390,11 @@ export function RevisionsPanel({ // --------------------------------------------------------------------------- function RevisionListSkeleton() { + const keys = useMemo(() => Array.from({ length: 5 }, (_, i) => i), []); return (
- {Array.from({ length: 5 }, (_, i) => ( -
+ {keys.map((k) => ( +
diff --git a/apps/dashboard/src/components/sidebar/app-sidebar.tsx b/apps/dashboard/src/components/sidebar/app-sidebar.tsx index 28494993..907916d4 100644 --- a/apps/dashboard/src/components/sidebar/app-sidebar.tsx +++ b/apps/dashboard/src/components/sidebar/app-sidebar.tsx @@ -30,7 +30,7 @@ import { useAuth } from "@/contexts/auth-context"; import { getCollections, getSites, siteRoleLabel } from "@/lib/api"; export function AppSidebar({ ...props }: ComponentProps) { - const { siteId } = useParams({ from: "/_admin/sites/$siteId" as any }); + const { siteId } = useParams({ from: "/_admin/sites/$siteId" }); const auth = useAuth(); const pathname = useRouterState({ select: (s) => s.location.pathname }); diff --git a/apps/dashboard/src/components/sidebar/site-switcher.tsx b/apps/dashboard/src/components/sidebar/site-switcher.tsx index 2a2743cb..df252a40 100644 --- a/apps/dashboard/src/components/sidebar/site-switcher.tsx +++ b/apps/dashboard/src/components/sidebar/site-switcher.tsx @@ -37,7 +37,7 @@ export function SiteSwitcher({ }) { const { isMobile } = useSidebar(); const navigate = useNavigate(); - const { siteId } = useParams({ from: "/_admin/sites/$siteId" as any }); + const { siteId } = useParams({ from: "/_admin/sites/$siteId" }); const [sidebarHovered, setSidebarHovered] = useState(false); const [hoveredSiteId, setHoveredSiteId] = useState(null); diff --git a/apps/dashboard/src/components/site-settings/user-combobox.tsx b/apps/dashboard/src/components/site-settings/user-combobox.tsx index 04680552..4e3f44d8 100644 --- a/apps/dashboard/src/components/site-settings/user-combobox.tsx +++ b/apps/dashboard/src/components/site-settings/user-combobox.tsx @@ -44,7 +44,6 @@ export function UserCombobox({ render={
diff --git a/apps/web/src/components/landing/database-section.tsx b/apps/web/src/components/landing/database-section.tsx index bbce36d0..0a2fc006 100644 --- a/apps/web/src/components/landing/database-section.tsx +++ b/apps/web/src/components/landing/database-section.tsx @@ -19,7 +19,7 @@ export function DatabaseSection() { ([entry]) => { if (entry.isIntersecting) setIsVisible(true); }, - { threshold: 0.1 } + { threshold: 0.1 }, ); if (sectionRef.current) observer.observe(sectionRef.current); @@ -34,24 +34,34 @@ export function DatabaseSection() { }, []); return ( -
- {/* Background accent — retiré, remplacé par l'image sphère */} - +
+ {/* Background accent — retiré, remplacé par l'image sphère */} +
{/* Header */}
- + Database Support - +
{/* Image globe — colonne gauche, pleine hauteur */} -
+
Global network sphere -

+

Your choice
of database.

-

- Embedded SQLite for lightweight workloads, MySQL for scalable deployments, PostgreSQL for complex, high-volume systems. +

+ Embedded SQLite for lightweight workloads, MySQL for scalable + deployments, PostgreSQL for complex, high-volume systems.

@@ -81,9 +98,13 @@ export function DatabaseSection() { {/* Main content grid */}
{/* Large stat card */} -
+
{/* Animated dots background with connecting lines */}
{/* SVG for connecting lines */} @@ -140,53 +161,76 @@ export function DatabaseSection() { /> ))}
- +
- 3 - databases + + 3 + + + databases +

- Built-in support for the most popular databases. Configure once, scale anywhere. + Built-in support for the most popular databases. Configure once, + scale anywhere.

{/* Stacked stat cards */}
-
- < 20MB - Binary size +
+ + < 20MB + + + Binary size +
- -
+ +
- Projects per instance + + Projects per instance +
{/* Region list */} -
+
{regions.map((region, index) => (
- + {region.status} diff --git a/apps/web/src/components/landing/features-section.tsx b/apps/web/src/components/landing/features-section.tsx index b3786494..aaa88554 100644 --- a/apps/web/src/components/landing/features-section.tsx +++ b/apps/web/src/components/landing/features-section.tsx @@ -6,25 +6,29 @@ const features = [ { number: "01", title: "Single Binary Deployment", - description: "Deploy Velopulent CMS as a single binary with built-in embedded dashboard. No dependencies, no complex setup required. Perfect for individuals, agencies, and enterprises.", + description: + "Deploy Velopulent CMS as a single binary with built-in embedded dashboard. No dependencies, no complex setup required. Perfect for individuals, agencies, and enterprises.", stats: { value: "<15MB", label: "memory footprint" }, }, { number: "02", title: "Multi-Protocol Support", - description: "Built-in REST, GraphQL, gRPC and MCP protocols in a single binary. Connect to any framework or tech stack with zero integration hassle.", + description: + "Built-in REST, GraphQL, gRPC and MCP protocols in a single binary. Connect to any framework or tech stack with zero integration hassle.", stats: { value: "4", label: "protocols supported" }, }, { number: "03", title: "Flexible Database Support", - description: "Works seamlessly with SQLite, MySQL, PostgreSQL and more. Configure once and scale across your infrastructure.", + description: + "Works seamlessly with SQLite, MySQL, PostgreSQL and more. Configure once and scale across your infrastructure.", stats: { value: "3+", label: "database engines" }, }, { number: "04", title: "Multi-Project Management", - description: "Manage multiple sites and projects from a single instance. Built-in access control with admin, editor, and viewer roles.", + description: + "Manage multiple sites and projects from a single instance. Built-in access control with admin, editor, and viewer roles.", stats: { value: "∞", label: "projects supported" }, }, ]; @@ -65,8 +69,8 @@ function ParticleVisualization() { const particles = Array.from({ length: COUNT }, (_, i) => { const seed = i * 1.618; return { - bx: ((seed * 127.1) % 1), - by: ((seed * 311.7) % 1), + bx: (seed * 127.1) % 1, + by: (seed * 311.7) % 1, phase: seed * Math.PI * 2, speed: 0.4 + (seed % 0.4), radius: 1.2 + (seed % 2.2), @@ -138,7 +142,7 @@ export function FeaturesSection() { ([entry]) => { if (entry.isIntersecting) setIsVisible(true); }, - { threshold: 0.1 } + { threshold: 0.1 }, ); if (sectionRef.current) observer.observe(sectionRef.current); @@ -162,7 +166,9 @@ export function FeaturesSection() {

Powerful @@ -171,10 +177,16 @@ export function FeaturesSection() {

-

- Everything you need to manage content at scale. From single projects to enterprise deployments with multiple sites and teams. +

+ Everything you need to manage content at scale. From single + projects to enterprise deployments with multiple sites and + teams.

@@ -183,9 +195,11 @@ export function FeaturesSection() { {/* Bento Grid Layout */}
{/* Large feature card */} -
setActiveFeature(0)} > @@ -193,7 +207,9 @@ export function FeaturesSection() {
- {features[0].number} + + {features[0].number} +

{features[0].title}

@@ -201,8 +217,12 @@ export function FeaturesSection() { {features[0].description}

- {features[0].stats.value} - {features[0].stats.label} + + {features[0].stats.value} + + + {features[0].stats.label} +
diff --git a/apps/web/src/components/landing/security-section.tsx b/apps/web/src/components/landing/security-section.tsx index 49c5f52f..c806091a 100644 --- a/apps/web/src/components/landing/security-section.tsx +++ b/apps/web/src/components/landing/security-section.tsx @@ -42,7 +42,7 @@ export function SecuritySection() { ([entry]) => { if (entry.isIntersecting) setIsVisible(true); }, - { threshold: 0.1 } + { threshold: 0.1 }, ); if (sectionRef.current) observer.observe(sectionRef.current); @@ -57,34 +57,47 @@ export function SecuritySection() { }, []); return ( -
+
{/* Background accent removed */} - +
{/* Header */}
- + Security - + {/* Title — full width */} -

+

Built for trust.
Auditable and open.

- + {/* Description — below title */} -
+

- 100% open source means you control your data. Role-based access control for teams. Complete audit history of all changes. + 100% open source means you control your data. Role-based access + control for teams. Complete audit history of all changes.

@@ -92,9 +105,13 @@ export function SecuritySection() { {/* Main content */}
{/* Large visual card */} -
+
{/* Dynamic feature image with cross-fade — desktop only */}
{securityFeatures.map((feature, index) => ( @@ -107,22 +124,28 @@ export function SecuritySection() { /> ))}
- +
- Your data + + Your data +
100% - Under your control + + Under your control +
- + {/* Certification badges */}
{certifications.map((cert, index) => ( @@ -138,8 +161,8 @@ export function SecuritySection() {
setActiveFeature(index)} >
-
+

{feature.title}

-

{feature.description}

+

+ {feature.description} +

diff --git a/apps/web/src/components/mdx.tsx b/apps/web/src/components/mdx.tsx index a640575f..62ce6e51 100644 --- a/apps/web/src/components/mdx.tsx +++ b/apps/web/src/components/mdx.tsx @@ -1,5 +1,5 @@ -import defaultMdxComponents from 'fumadocs-ui/mdx'; -import type { MDXComponents } from 'mdx/types'; +import defaultMdxComponents from "fumadocs-ui/mdx"; +import type { MDXComponents } from "mdx/types"; export function getMDXComponents(components?: MDXComponents) { return { diff --git a/apps/web/src/components/provider.tsx b/apps/web/src/components/provider.tsx index 522282b2..6f92c0c4 100644 --- a/apps/web/src/components/provider.tsx +++ b/apps/web/src/components/provider.tsx @@ -1,7 +1,7 @@ -'use client'; -import SearchDialog from '@/components/search'; -import { RootProvider } from 'fumadocs-ui/provider/next'; -import { type ReactNode } from 'react'; +"use client"; +import SearchDialog from "@/components/search"; +import { RootProvider } from "fumadocs-ui/provider/next"; +import { type ReactNode } from "react"; export function Provider({ children }: { children: ReactNode }) { return {children}; diff --git a/apps/web/src/components/search.tsx b/apps/web/src/components/search.tsx index 1f704205..a88ce513 100644 --- a/apps/web/src/components/search.tsx +++ b/apps/web/src/components/search.tsx @@ -1,4 +1,4 @@ -'use client'; +"use client"; import { SearchDialog, SearchDialogClose, @@ -9,29 +9,34 @@ import { SearchDialogList, SearchDialogOverlay, type SharedProps, -} from 'fumadocs-ui/components/dialog/search'; -import { useDocsSearch } from 'fumadocs-core/search/client'; -import { create } from '@orama/orama'; -import { useI18n } from 'fumadocs-ui/contexts/i18n'; +} from "fumadocs-ui/components/dialog/search"; +import { useDocsSearch } from "fumadocs-core/search/client"; +import { create } from "@orama/orama"; +import { useI18n } from "fumadocs-ui/contexts/i18n"; function initOrama() { return create({ - schema: { _: 'string' }, + schema: { _: "string" }, // https://docs.orama.com/docs/orama-js/supported-languages - language: 'english', + language: "english", }); } export default function DefaultSearchDialog(props: SharedProps) { const { locale } = useI18n(); // (optional) for i18n const { search, setSearch, query } = useDocsSearch({ - type: 'static', + type: "static", initOrama, locale, }); return ( - + @@ -39,7 +44,7 @@ export default function DefaultSearchDialog(props: SharedProps) { - + ); diff --git a/apps/web/src/components/ui/button.tsx b/apps/web/src/components/ui/button.tsx index 6138844d..fb5b30b3 100644 --- a/apps/web/src/components/ui/button.tsx +++ b/apps/web/src/components/ui/button.tsx @@ -1,8 +1,8 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" -import { Slot } from "radix-ui" +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { Slot } from "radix-ui"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const buttonVariants = cva( "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", @@ -38,8 +38,8 @@ const buttonVariants = cva( variant: "default", size: "default", }, - } -) + }, +); function Button({ className, @@ -49,9 +49,9 @@ function Button({ ...props }: React.ComponentProps<"button"> & VariantProps & { - asChild?: boolean + asChild?: boolean; }) { - const Comp = asChild ? Slot.Root : "button" + const Comp = asChild ? Slot.Root : "button"; return ( - ) + ); } -export { Button, buttonVariants } +export { Button, buttonVariants }; diff --git a/apps/web/src/components/ui/select.tsx b/apps/web/src/components/ui/select.tsx index f09dfb48..88d61f2d 100644 --- a/apps/web/src/components/ui/select.tsx +++ b/apps/web/src/components/ui/select.tsx @@ -1,15 +1,15 @@ -"use client" +"use client"; -import * as React from "react" -import { Select as SelectPrimitive } from "radix-ui" +import * as React from "react"; +import { Select as SelectPrimitive } from "radix-ui"; -import { cn } from "@/lib/utils" -import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react" +import { cn } from "@/lib/utils"; +import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"; function Select({ ...props }: React.ComponentProps) { - return + return ; } function SelectGroup({ @@ -22,13 +22,13 @@ function SelectGroup({ className={cn("scroll-my-1 p-1", className)} {...props} /> - ) + ); } function SelectValue({ ...props }: React.ComponentProps) { - return + return ; } function SelectTrigger({ @@ -37,7 +37,7 @@ function SelectTrigger({ children, ...props }: React.ComponentProps & { - size?: "sm" | "default" + size?: "sm" | "default"; }) { return ( @@ -54,7 +54,7 @@ function SelectTrigger({ - ) + ); } function SelectContent({ @@ -69,7 +69,12 @@ function SelectContent({ {children} @@ -87,7 +92,7 @@ function SelectContent({ - ) + ); } function SelectLabel({ @@ -100,7 +105,7 @@ function SelectLabel({ className={cn("px-1.5 py-1 text-xs text-muted-foreground", className)} {...props} /> - ) + ); } function SelectItem({ @@ -113,7 +118,7 @@ function SelectItem({ data-slot="select-item" className={cn( "relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", - className + className, )} {...props} > @@ -124,7 +129,7 @@ function SelectItem({ {children} - ) + ); } function SelectSeparator({ @@ -137,7 +142,7 @@ function SelectSeparator({ className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)} {...props} /> - ) + ); } function SelectScrollUpButton({ @@ -149,14 +154,13 @@ function SelectScrollUpButton({ data-slot="select-scroll-up-button" className={cn( "z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4", - className + className, )} {...props} > - + - ) + ); } function SelectScrollDownButton({ @@ -168,14 +172,13 @@ function SelectScrollDownButton({ data-slot="select-scroll-down-button" className={cn( "z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4", - className + className, )} {...props} > - + - ) + ); } export { @@ -189,4 +192,4 @@ export { SelectSeparator, SelectTrigger, SelectValue, -} +}; diff --git a/apps/web/src/lib/cn.ts b/apps/web/src/lib/cn.ts index ba66fd25..8e473dac 100644 --- a/apps/web/src/lib/cn.ts +++ b/apps/web/src/lib/cn.ts @@ -1 +1 @@ -export { twMerge as cn } from 'tailwind-merge'; +export { twMerge as cn } from "tailwind-merge"; diff --git a/apps/web/src/lib/layout.shared.tsx b/apps/web/src/lib/layout.shared.tsx index 88b9ca86..ff20abdc 100644 --- a/apps/web/src/lib/layout.shared.tsx +++ b/apps/web/src/lib/layout.shared.tsx @@ -1,5 +1,5 @@ -import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; -import { appName, gitConfig } from './shared'; +import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared"; +import { appName, gitConfig } from "./shared"; export function baseOptions(): BaseLayoutProps { return { diff --git a/apps/web/src/lib/shared.ts b/apps/web/src/lib/shared.ts index 96f75463..743d25f4 100644 --- a/apps/web/src/lib/shared.ts +++ b/apps/web/src/lib/shared.ts @@ -1,11 +1,11 @@ -export const appName = 'Velopulent CMS'; -export const docsRoute = '/docs'; -export const docsImageRoute = '/og/docs'; -export const docsContentRoute = '/llms.mdx/docs'; +export const appName = "Velopulent CMS"; +export const docsRoute = "/docs"; +export const docsImageRoute = "/og/docs"; +export const docsContentRoute = "/llms.mdx/docs"; // fill this with your actual GitHub info, for example: export const gitConfig = { - user: 'velopulent', - repo: 'cms', - branch: 'main', + user: "velopulent", + repo: "cms", + branch: "main", }; diff --git a/apps/web/src/lib/source.ts b/apps/web/src/lib/source.ts index f5d0eae2..47710c0b 100644 --- a/apps/web/src/lib/source.ts +++ b/apps/web/src/lib/source.ts @@ -1,6 +1,6 @@ -import { docs } from 'collections/server'; -import { loader } from 'fumadocs-core/source'; -import { docsContentRoute, docsImageRoute, docsRoute } from './shared'; +import { docs } from "collections/server"; +import { loader } from "fumadocs-core/source"; +import { docsContentRoute, docsImageRoute, docsRoute } from "./shared"; // See https://fumadocs.dev/docs/headless/source-api for more info export const source = loader({ @@ -9,26 +9,26 @@ export const source = loader({ plugins: [], }); -export function getPageImage(page: (typeof source)['$inferPage']) { - const segments = [...page.slugs, 'image.png']; +export function getPageImage(page: (typeof source)["$inferPage"]) { + const segments = [...page.slugs, "image.png"]; return { segments, - url: `${docsImageRoute}/${segments.join('/')}`, + url: `${docsImageRoute}/${segments.join("/")}`, }; } -export function getPageMarkdownUrl(page: (typeof source)['$inferPage']) { - const segments = [...page.slugs, 'content.md']; +export function getPageMarkdownUrl(page: (typeof source)["$inferPage"]) { + const segments = [...page.slugs, "content.md"]; return { segments, - url: `${docsContentRoute}/${segments.join('/')}`, + url: `${docsContentRoute}/${segments.join("/")}`, }; } -export async function getLLMText(page: (typeof source)['$inferPage']) { - const processed = await page.data.getText('processed'); +export async function getLLMText(page: (typeof source)["$inferPage"]) { + const processed = await page.data.getText("processed"); return `# ${page.data.title} (${page.url}) diff --git a/apps/web/src/lib/utils.ts b/apps/web/src/lib/utils.ts index bd0c391d..a5ef1935 100644 --- a/apps/web/src/lib/utils.ts +++ b/apps/web/src/lib/utils.ts @@ -1,6 +1,6 @@ -import { clsx, type ClassValue } from "clsx" -import { twMerge } from "tailwind-merge" +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)); } diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 59c41baa..f43f8735 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ESNext", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -19,12 +15,8 @@ "jsx": "react-jsx", "incremental": true, "paths": { - "@/*": [ - "./src/*" - ], - "collections/*": [ - "./.source/*" - ] + "@/*": ["./src/*"], + "collections/*": ["./.source/*"] }, "plugins": [ { @@ -39,7 +31,5 @@ ".next/types/**/*.ts", ".next/dev/types/**/*.ts" ], - "exclude": [ - "node_modules" - ] -} \ No newline at end of file + "exclude": ["node_modules"] +} From 2d6d0ce899a9e9f94b356e0a8f54c788d81dc1a3 Mon Sep 17 00:00:00 2001 From: Krishna Santosh <75202541+krishna-santosh@users.noreply.github.com> Date: Sun, 21 Jun 2026 09:18:24 +0530 Subject: [PATCH 31/43] fix(workflows): enhance release workflow for multi-architecture support and caching --- .github/workflows/release.yml | 111 ++++++++++++++++++++++++---------- 1 file changed, 79 insertions(+), 32 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 556cbd3f..936be0ef 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,8 +1,12 @@ name: Release -# Tag-driven. Push a tag like `v1.2.3` to build per-OS binaries and attach them to -# a GitHub Release. The release binary embeds apps/dashboard/dist via rust-embed, +# Tag-driven. Push a tag like `v1.2.3` to build per-OS/arch binaries and attach them +# to a GitHub Release. The release binary embeds apps/dashboard/dist via rust-embed, # so the dashboard MUST be built before `cargo build --release`. +# +# The dashboard dist is static (JS/HTML) and identical for every target, so it is +# built ONCE here and shared to all build jobs as an artifact. The build matrix then +# runs natively on each OS/arch (no cross-compilation) and compiles cargo directly. on: push: tags: @@ -12,57 +16,100 @@ permissions: contents: write jobs: + # Build the static dashboard once. Every build job consumes this same dist. + dashboard: + name: Build dashboard + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + # Persist the Nx local cache across runs. Ephemeral runners wipe .nx/cache, so + # without this Nx caching is a no-op. This lets dashboard:build (8-10s) replay + # from cache instead of rebuilding. + - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: .nx/cache + key: nx-${{ runner.os }}-${{ hashFiles('apps/dashboard/**', 'bun.lock', 'package.json', 'nx.json') }} + restore-keys: | + nx-${{ runner.os }}- + - name: Build dashboard + run: | + bun install --frozen-lockfile + bunx nx run dashboard:build + - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + with: + name: dashboard-dist + path: apps/dashboard/dist + if-no-files-found: error + build: - name: Build (${{ matrix.target }}) - runs-on: ${{ matrix.os }} + name: Build (${{ matrix.os }}-${{ matrix.arch }}) + needs: dashboard + runs-on: ${{ matrix.runner }} strategy: fail-fast: false matrix: include: - - os: ubuntu-latest - target: x86_64-unknown-linux-gnu + - os: linux + arch: amd64 + runner: ubuntu-latest bin: cms - asset: cms-x86_64-linux - - os: windows-latest - target: x86_64-pc-windows-msvc - bin: cms.exe - asset: cms-x86_64-windows.exe - - os: macos-latest - target: aarch64-apple-darwin + asset: cms-${{ github.ref_name }}-linux-amd64 + - os: linux + arch: arm64 + runner: ubuntu-24.04-arm + bin: cms + asset: cms-${{ github.ref_name }}-linux-arm64 + - os: macos + arch: amd64 + runner: macos-13 + bin: cms + asset: cms-${{ github.ref_name }}-macos-amd64 + - os: macos + arch: arm64 + runner: macos-14 bin: cms - asset: cms-aarch64-macos + asset: cms-${{ github.ref_name }}-macos-arm64 + - os: windows + arch: amd64 + runner: windows-latest + bin: cms.exe + asset: cms-${{ github.ref_name }}-windows-amd64.exe + - os: windows + arch: arm64 + runner: windows-11-arm + bin: cms.exe + asset: cms-${{ github.ref_name }}-windows-arm64.exe steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: persist-credentials: false + # rust-embed (#[folder = "../dashboard/dist"]) needs the built dashboard present + # at compile time. Pull the shared artifact into apps/dashboard/dist. + - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: dashboard-dist + path: apps/dashboard/dist - uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + # Installs the host toolchain, which on each runner is already the target arch + # (native build, no --target needed). - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # master with: toolchain: "1.94" - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: workspaces: apps/backend - key: release-${{ matrix.target }} - # Persist the Nx local cache across runs. Ephemeral runners wipe .nx/cache, so - # without this Nx caching is a no-op. This lets dashboard:build (8-10s) replay - # from cache instead of rebuilding; the Rust compile stays on rust-cache above. - - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 - with: - path: .nx/cache - key: nx-${{ runner.os }}-${{ hashFiles('apps/dashboard/**', 'bun.lock', 'package.json', 'nx.json') }} - restore-keys: | - nx-${{ runner.os }}- + key: release-${{ matrix.os }}-${{ matrix.arch }} - # Single Nx command owns the dashboard -> backend ordering: backend:build - # dependsOn dashboard:build (rust-embed bakes apps/dashboard/dist into the - # binary) and sets SKIP_DASHBOARD_BUILD=1 so cargo consumes the built dist. - - name: Build release binary (dashboard + backend via Nx) - run: | - bun install --frozen-lockfile - bunx nx run backend:build + # Build cargo directly (dist is already present from the artifact); keep the + # default `embed-dashboard` feature so rust-embed bakes in apps/dashboard/dist. + - name: Build release binary + working-directory: apps/backend + run: cargo build --release --locked - name: Stage artifact shell: bash From bf80677c34f8a8128b9ca3fda98e3e22fe9c6caa Mon Sep 17 00:00:00 2001 From: Krishna Santosh <75202541+krishna-santosh@users.noreply.github.com> Date: Sun, 21 Jun 2026 09:24:21 +0530 Subject: [PATCH 32/43] style: sort imports --- apps/web/source.config.ts | 2 +- apps/web/src/app/(home)/_download/page.tsx | 8 ++++---- apps/web/src/app/(home)/layout.tsx | 2 +- apps/web/src/app/(home)/page.tsx | 16 ++++++++-------- apps/web/src/app/api/search/route.ts | 2 +- apps/web/src/app/docs/[[...slug]]/page.tsx | 6 +++--- apps/web/src/app/docs/layout.tsx | 6 +++--- apps/web/src/app/layout.tsx | 2 +- .../src/app/llms.mdx/docs/[[...slug]]/route.ts | 2 +- apps/web/src/app/llms.txt/route.ts | 2 +- apps/web/src/app/og/docs/[...slug]/route.tsx | 4 ++-- apps/web/src/components/landing/ascii-scene.tsx | 2 +- .../src/components/landing/backup-section.tsx | 4 ++-- apps/web/src/components/landing/cta-section.tsx | 8 ++++++-- .../src/components/landing/dashboard-section.tsx | 4 ++-- .../src/components/landing/database-section.tsx | 2 +- .../src/components/landing/footer-section.tsx | 1 + apps/web/src/components/landing/hero-section.tsx | 4 ++-- .../components/landing/integrations-section.tsx | 2 +- apps/web/src/components/landing/navigation.tsx | 4 ++-- .../src/components/landing/security-section.tsx | 4 ++-- .../components/landing/testimonials-section.tsx | 2 +- apps/web/src/components/landing/use-cases.tsx | 2 +- apps/web/src/components/provider.tsx | 4 ++-- apps/web/src/components/search.tsx | 4 ++-- apps/web/src/lib/utils.ts | 2 +- 26 files changed, 53 insertions(+), 48 deletions(-) diff --git a/apps/web/source.config.ts b/apps/web/source.config.ts index 43aeb805..4cef0055 100644 --- a/apps/web/source.config.ts +++ b/apps/web/source.config.ts @@ -1,5 +1,5 @@ -import { defineConfig, defineDocs } from "fumadocs-mdx/config"; import { metaSchema, pageSchema } from "fumadocs-core/source/schema"; +import { defineConfig, defineDocs } from "fumadocs-mdx/config"; // You can customize Zod schemas for frontmatter and `meta.json` here // see https://fumadocs.dev/docs/mdx/collections diff --git a/apps/web/src/app/(home)/_download/page.tsx b/apps/web/src/app/(home)/_download/page.tsx index df0d9a76..e6b2d57c 100644 --- a/apps/web/src/app/(home)/_download/page.tsx +++ b/apps/web/src/app/(home)/_download/page.tsx @@ -1,8 +1,11 @@ "use client"; +import { Icon } from "@iconify-icon/react"; +import { Check, Code2, Download } from "lucide-react"; +import Image from "next/image"; import { useState } from "react"; -import { Navigation } from "@/components/landing/navigation"; import { FooterSection } from "@/components/landing/footer-section"; +import { Navigation } from "@/components/landing/navigation"; import { Button } from "@/components/ui/button"; import { Select, @@ -11,9 +14,6 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { Check, Download, Code2 } from "lucide-react"; -import { Icon } from "@iconify-icon/react"; -import Image from "next/image"; type OS = "linux" | "macos" | "windows"; type Architecture = "x86_64" | "aarch64" | "arm64"; diff --git a/apps/web/src/app/(home)/layout.tsx b/apps/web/src/app/(home)/layout.tsx index 94893fae..8f356971 100644 --- a/apps/web/src/app/(home)/layout.tsx +++ b/apps/web/src/app/(home)/layout.tsx @@ -1,10 +1,10 @@ -import React from "react"; import type { Metadata } from "next"; import { Instrument_Sans, Instrument_Serif, JetBrains_Mono, } from "next/font/google"; +import type React from "react"; import "./home.css"; import { Navigation } from "@/components/landing/navigation"; diff --git a/apps/web/src/app/(home)/page.tsx b/apps/web/src/app/(home)/page.tsx index c3504e21..308aef62 100644 --- a/apps/web/src/app/(home)/page.tsx +++ b/apps/web/src/app/(home)/page.tsx @@ -1,16 +1,16 @@ -import { Navigation } from "@/components/landing/navigation"; -import { HeroSection } from "@/components/landing/hero-section"; +import { BackupSection } from "@/components/landing/backup-section"; +import { CtaSection } from "@/components/landing/cta-section"; +import { DashboardSection } from "@/components/landing/dashboard-section"; +import { DatabaseSection } from "@/components/landing/database-section"; import { FeaturesSection } from "@/components/landing/features-section"; +import { FooterSection } from "@/components/landing/footer-section"; +import { HeroSection } from "@/components/landing/hero-section"; import { HowItWorksSection } from "@/components/landing/how-it-works-section"; -import { DatabaseSection } from "@/components/landing/database-section"; -import { DashboardSection } from "@/components/landing/dashboard-section"; import { IntegrationsSection } from "@/components/landing/integrations-section"; +import { Navigation } from "@/components/landing/navigation"; import { SecuritySection } from "@/components/landing/security-section"; -import { UseCasesSection } from "@/components/landing/use-cases"; import { TestimonialsSection } from "@/components/landing/testimonials-section"; -import { CtaSection } from "@/components/landing/cta-section"; -import { FooterSection } from "@/components/landing/footer-section"; -import { BackupSection } from "@/components/landing/backup-section"; +import { UseCasesSection } from "@/components/landing/use-cases"; export default function Home() { return ( diff --git a/apps/web/src/app/api/search/route.ts b/apps/web/src/app/api/search/route.ts index 85448f9d..c312b919 100644 --- a/apps/web/src/app/api/search/route.ts +++ b/apps/web/src/app/api/search/route.ts @@ -1,5 +1,5 @@ -import { source } from "@/lib/source"; import { createFromSource } from "fumadocs-core/search/server"; +import { source } from "@/lib/source"; export const revalidate = false; diff --git a/apps/web/src/app/docs/[[...slug]]/page.tsx b/apps/web/src/app/docs/[[...slug]]/page.tsx index 54115be6..074d053a 100644 --- a/apps/web/src/app/docs/[[...slug]]/page.tsx +++ b/apps/web/src/app/docs/[[...slug]]/page.tsx @@ -1,4 +1,3 @@ -import { getPageImage, getPageMarkdownUrl, source } from "@/lib/source"; import { DocsBody, DocsDescription, @@ -7,11 +6,12 @@ import { MarkdownCopyButton, ViewOptionsPopover, } from "fumadocs-ui/layouts/docs/page"; +import { createRelativeLink } from "fumadocs-ui/mdx"; +import type { Metadata } from "next"; import { notFound } from "next/navigation"; import { getMDXComponents } from "@/components/mdx"; -import type { Metadata } from "next"; -import { createRelativeLink } from "fumadocs-ui/mdx"; import { gitConfig } from "@/lib/shared"; +import { getPageImage, getPageMarkdownUrl, source } from "@/lib/source"; export default async function Page(props: PageProps<"/docs/[[...slug]]">) { const params = await props.params; diff --git a/apps/web/src/app/docs/layout.tsx b/apps/web/src/app/docs/layout.tsx index 5f237b0b..7ecb42d2 100644 --- a/apps/web/src/app/docs/layout.tsx +++ b/apps/web/src/app/docs/layout.tsx @@ -1,10 +1,10 @@ import "./docs.css"; -import { source } from "@/lib/source"; import { DocsLayout } from "fumadocs-ui/layouts/docs"; -import { baseOptions } from "@/lib/layout.shared"; -import { Provider as FumadocsProvider } from "@/components/provider"; import { Geist, Inter } from "next/font/google"; +import { Provider as FumadocsProvider } from "@/components/provider"; import { cn } from "@/lib/cn"; +import { baseOptions } from "@/lib/layout.shared"; +import { source } from "@/lib/source"; const geist = Geist({ subsets: ["latin"], variable: "--font-sans" }); diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 787e60cc..f37e6c76 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -1,5 +1,5 @@ import "./global.css"; -import { Metadata } from "next"; +import type { Metadata } from "next"; export const metadata: Metadata = { title: "Velopulent CMS", diff --git a/apps/web/src/app/llms.mdx/docs/[[...slug]]/route.ts b/apps/web/src/app/llms.mdx/docs/[[...slug]]/route.ts index 1ccfc9c3..f6f13190 100644 --- a/apps/web/src/app/llms.mdx/docs/[[...slug]]/route.ts +++ b/apps/web/src/app/llms.mdx/docs/[[...slug]]/route.ts @@ -1,5 +1,5 @@ -import { getLLMText, getPageMarkdownUrl, source } from "@/lib/source"; import { notFound } from "next/navigation"; +import { getLLMText, getPageMarkdownUrl, source } from "@/lib/source"; export const revalidate = false; diff --git a/apps/web/src/app/llms.txt/route.ts b/apps/web/src/app/llms.txt/route.ts index f18f409c..775be71f 100644 --- a/apps/web/src/app/llms.txt/route.ts +++ b/apps/web/src/app/llms.txt/route.ts @@ -1,5 +1,5 @@ -import { source } from "@/lib/source"; import { llms } from "fumadocs-core/source"; +import { source } from "@/lib/source"; export const revalidate = false; diff --git a/apps/web/src/app/og/docs/[...slug]/route.tsx b/apps/web/src/app/og/docs/[...slug]/route.tsx index 451e98ea..5e951cf1 100644 --- a/apps/web/src/app/og/docs/[...slug]/route.tsx +++ b/apps/web/src/app/og/docs/[...slug]/route.tsx @@ -1,8 +1,8 @@ -import { getPageImage, source } from "@/lib/source"; +import { generate as DefaultImage } from "fumadocs-ui/og"; import { notFound } from "next/navigation"; import { ImageResponse } from "next/og"; -import { generate as DefaultImage } from "fumadocs-ui/og"; import { appName } from "@/lib/shared"; +import { getPageImage, source } from "@/lib/source"; export const revalidate = false; diff --git a/apps/web/src/components/landing/ascii-scene.tsx b/apps/web/src/components/landing/ascii-scene.tsx index 6535c65a..382a2455 100644 --- a/apps/web/src/components/landing/ascii-scene.tsx +++ b/apps/web/src/components/landing/ascii-scene.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useRef, useCallback } from "react"; +import { useCallback, useEffect, useRef } from "react"; interface Point3D { x: number; diff --git a/apps/web/src/components/landing/backup-section.tsx b/apps/web/src/components/landing/backup-section.tsx index 82babab3..eca26f21 100644 --- a/apps/web/src/components/landing/backup-section.tsx +++ b/apps/web/src/components/landing/backup-section.tsx @@ -1,7 +1,7 @@ "use client"; -import { useEffect, useState, useRef } from "react"; -import { HardDrive, Cloud, Shield, RotateCcw } from "lucide-react"; +import { Cloud, HardDrive, RotateCcw, Shield } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; const backupFeatures = [ { diff --git a/apps/web/src/components/landing/cta-section.tsx b/apps/web/src/components/landing/cta-section.tsx index cfb8c29a..21aca4ed 100644 --- a/apps/web/src/components/landing/cta-section.tsx +++ b/apps/web/src/components/landing/cta-section.tsx @@ -1,8 +1,8 @@ "use client"; +import { ArrowRight } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; -import { ArrowRight } from "lucide-react"; export function CtaSection() { const [isVisible, setIsVisible] = useState(false); @@ -81,7 +81,11 @@ export function CtaSection() { className="h-14 px-8 text-base rounded-full border-foreground/20 hover:bg-foreground/5" asChild > - + Github diff --git a/apps/web/src/components/landing/dashboard-section.tsx b/apps/web/src/components/landing/dashboard-section.tsx index 8ecd4897..c182352a 100644 --- a/apps/web/src/components/landing/dashboard-section.tsx +++ b/apps/web/src/components/landing/dashboard-section.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; const dashboardCards = [ { @@ -47,7 +47,7 @@ function AnimatedNumber({ const animate = (currentTime: number) => { const elapsed = currentTime - startTime; const progress = Math.min(elapsed / duration, 1); - const eased = 1 - Math.pow(1 - progress, 4); + const eased = 1 - (1 - progress) ** 4; setCount(Math.floor(eased * end)); setIsScrambling(progress < 0.8); if (progress < 1) requestAnimationFrame(animate); diff --git a/apps/web/src/components/landing/database-section.tsx b/apps/web/src/components/landing/database-section.tsx index 0a2fc006..6d4b7e26 100644 --- a/apps/web/src/components/landing/database-section.tsx +++ b/apps/web/src/components/landing/database-section.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; const regions = [ { name: "SQLite", nodes: 0, status: "supported" }, diff --git a/apps/web/src/components/landing/footer-section.tsx b/apps/web/src/components/landing/footer-section.tsx index f6384af8..ff435835 100644 --- a/apps/web/src/components/landing/footer-section.tsx +++ b/apps/web/src/components/landing/footer-section.tsx @@ -141,6 +141,7 @@ export function FooterSection() { href={link.href} target="_blank" className="text-sm text-white/40 hover:text-white transition-colors flex items-center gap-1 group" + rel="noopener" > {link.name} diff --git a/apps/web/src/components/landing/hero-section.tsx b/apps/web/src/components/landing/hero-section.tsx index a7a1aaa7..ffee26f4 100644 --- a/apps/web/src/components/landing/hero-section.tsx +++ b/apps/web/src/components/landing/hero-section.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; const words = ["website", "blog", "app", "business", "portfolio", "platform"]; @@ -33,7 +33,7 @@ function BlurWord({ word, trigger }: { word: string; trigger: number }) { const start = performance.now(); const tick = (now: number) => { const progress = Math.min((now - start) / DURATION, 1); - const eased = 1 - Math.pow(1 - progress, 3); + const eased = 1 - (1 - progress) ** 3; setLetterStates((prev) => { const next = [...prev]; next[i] = { opacity: eased, blur: 20 * (1 - eased) }; diff --git a/apps/web/src/components/landing/integrations-section.tsx b/apps/web/src/components/landing/integrations-section.tsx index 8ff8cb06..6f10fa00 100644 --- a/apps/web/src/components/landing/integrations-section.tsx +++ b/apps/web/src/components/landing/integrations-section.tsx @@ -1,8 +1,8 @@ "use client"; -import { useEffect, useState, useRef } from "react"; import { Icon } from "@iconify-icon/react"; import { LucideArrowRight } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; const frameworks = [ { name: "Next.js", icon: "logos:nextjs-icon" }, diff --git a/apps/web/src/components/landing/navigation.tsx b/apps/web/src/components/landing/navigation.tsx index 7810be08..76e90054 100644 --- a/apps/web/src/components/landing/navigation.tsx +++ b/apps/web/src/components/landing/navigation.tsx @@ -1,9 +1,9 @@ "use client"; -import { useState, useEffect } from "react"; -import { Button } from "@/components/ui/button"; import { Menu, X } from "lucide-react"; import Link from "next/link"; +import { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; const navLinks = [ { name: "Features", href: "#features" }, diff --git a/apps/web/src/components/landing/security-section.tsx b/apps/web/src/components/landing/security-section.tsx index c806091a..156399a9 100644 --- a/apps/web/src/components/landing/security-section.tsx +++ b/apps/web/src/components/landing/security-section.tsx @@ -1,7 +1,7 @@ "use client"; -import { useEffect, useState, useRef } from "react"; -import { Shield, Lock, Eye, FileCheck } from "lucide-react"; +import { Eye, FileCheck, Lock, Shield } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; const securityFeatures = [ { diff --git a/apps/web/src/components/landing/testimonials-section.tsx b/apps/web/src/components/landing/testimonials-section.tsx index 728bf32a..b3c45116 100644 --- a/apps/web/src/components/landing/testimonials-section.tsx +++ b/apps/web/src/components/landing/testimonials-section.tsx @@ -1,7 +1,7 @@ "use client"; -import { useEffect, useState, useRef } from "react"; import { ArrowLeft, ArrowRight } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; const testimonials = [ { diff --git a/apps/web/src/components/landing/use-cases.tsx b/apps/web/src/components/landing/use-cases.tsx index 86db896e..54702723 100644 --- a/apps/web/src/components/landing/use-cases.tsx +++ b/apps/web/src/components/landing/use-cases.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; const useCases = [ { diff --git a/apps/web/src/components/provider.tsx b/apps/web/src/components/provider.tsx index 6f92c0c4..de8ad125 100644 --- a/apps/web/src/components/provider.tsx +++ b/apps/web/src/components/provider.tsx @@ -1,7 +1,7 @@ "use client"; -import SearchDialog from "@/components/search"; import { RootProvider } from "fumadocs-ui/provider/next"; -import { type ReactNode } from "react"; +import type { ReactNode } from "react"; +import SearchDialog from "@/components/search"; export function Provider({ children }: { children: ReactNode }) { return {children}; diff --git a/apps/web/src/components/search.tsx b/apps/web/src/components/search.tsx index a88ce513..c46e11ef 100644 --- a/apps/web/src/components/search.tsx +++ b/apps/web/src/components/search.tsx @@ -1,4 +1,6 @@ "use client"; +import { create } from "@orama/orama"; +import { useDocsSearch } from "fumadocs-core/search/client"; import { SearchDialog, SearchDialogClose, @@ -10,8 +12,6 @@ import { SearchDialogOverlay, type SharedProps, } from "fumadocs-ui/components/dialog/search"; -import { useDocsSearch } from "fumadocs-core/search/client"; -import { create } from "@orama/orama"; import { useI18n } from "fumadocs-ui/contexts/i18n"; function initOrama() { diff --git a/apps/web/src/lib/utils.ts b/apps/web/src/lib/utils.ts index a5ef1935..365058ce 100644 --- a/apps/web/src/lib/utils.ts +++ b/apps/web/src/lib/utils.ts @@ -1,4 +1,4 @@ -import { clsx, type ClassValue } from "clsx"; +import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { From 67382ff0c98c61c34ad30de076c2958ac783d05b Mon Sep 17 00:00:00 2001 From: Krishna Santosh <75202541+krishna-santosh@users.noreply.github.com> Date: Sun, 21 Jun 2026 09:31:50 +0530 Subject: [PATCH 33/43] fix(ci): replace Biome lint with Biome CI for dashboard and web jobs --- .github/workflows/ci.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8f5b359..495e823a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -239,8 +239,9 @@ jobs: key: nx-${{ runner.os }}-${{ hashFiles('apps/dashboard/**', 'bun.lock', 'package.json', 'nx.json') }} restore-keys: | nx-${{ runner.os }}- - - name: Biome lint - run: bunx nx run dashboard:lint + - name: Biome CI + run: bunx biome ci + working-directory: apps/dashboard - name: Typecheck run: bunx nx run dashboard:typecheck - name: Build @@ -263,8 +264,9 @@ jobs: key: nx-${{ runner.os }}-${{ hashFiles('apps/web/**', 'bun.lock', 'package.json', 'nx.json') }} restore-keys: | nx-${{ runner.os }}- - - name: Biome lint - run: bunx nx run web:lint + - name: Biome CI + run: bunx biome ci + working-directory: apps/web - name: Typecheck run: bunx nx run web:typecheck - name: Build From e382e3e671fcbe2f9786ee926e08e9e0946761d4 Mon Sep 17 00:00:00 2001 From: Krishna Santosh <75202541+krishna-santosh@users.noreply.github.com> Date: Sun, 21 Jun 2026 09:33:27 +0530 Subject: [PATCH 34/43] style(dashboard): formatting --- .../src/components/app-breadcrumb.tsx | 70 +++- .../components/backups/backups-section.tsx | 209 +++++++++--- .../src/components/dashboard-header.tsx | 6 +- .../dashboard/src/components/dynamic-form.tsx | 42 ++- .../src/components/file-picker-dialog.tsx | 300 +++++++++--------- .../src/components/revisions-panel.tsx | 17 +- .../src/components/sidebar/site-switcher.tsx | 6 +- apps/dashboard/src/components/site-avatar.tsx | 86 ++--- .../site-settings/members-section.tsx | 4 +- .../site-settings/user-combobox.tsx | 9 +- .../src/components/tiptap-editor.tsx | 7 +- apps/dashboard/src/contexts/auth-context.tsx | 4 +- apps/dashboard/src/lib/api.ts | 89 ++++-- .../src/routes/_admin/_shell/index.tsx | 4 +- .../src/routes/_admin/_shell/settings.tsx | 42 +-- .../src/routes/_admin/sites.$siteId.tsx | 6 +- .../_admin/sites.$siteId/collections.tsx | 5 +- .../routes/_admin/sites.$siteId/settings.tsx | 99 +++--- apps/dashboard/src/styles.css | 4 +- 19 files changed, 606 insertions(+), 403 deletions(-) diff --git a/apps/dashboard/src/components/app-breadcrumb.tsx b/apps/dashboard/src/components/app-breadcrumb.tsx index 1006304a..4977fcd5 100644 --- a/apps/dashboard/src/components/app-breadcrumb.tsx +++ b/apps/dashboard/src/components/app-breadcrumb.tsx @@ -1,6 +1,6 @@ -import { Fragment } from "react"; -import { useMatches, useParams, Link } from "@tanstack/react-router"; import { useQuery } from "@tanstack/react-query"; +import { Link, useMatches, useParams } from "@tanstack/react-router"; +import { Fragment } from "react"; import { Breadcrumb, BreadcrumbItem, @@ -27,11 +27,19 @@ interface BreadcrumbConfig { const breadcrumbConfigs: BreadcrumbConfig[] = [ { routeId: "/_admin/sites/$siteId/entries/$collectionSlug/$id/edit", - crumbs: [{ labelFrom: "site" }, { labelFrom: "collection" }, { labelFrom: "entrySlug" }], + crumbs: [ + { labelFrom: "site" }, + { labelFrom: "collection" }, + { labelFrom: "entrySlug" }, + ], }, { routeId: "/_admin/sites/$siteId/entries/$collectionSlug/new", - crumbs: [{ labelFrom: "site" }, { labelFrom: "collection" }, { label: "New" }], + crumbs: [ + { labelFrom: "site" }, + { labelFrom: "collection" }, + { label: "New" }, + ], }, { routeId: "/_admin/sites/$siteId/entries/$collectionSlug/", @@ -77,27 +85,40 @@ function useSiteName(siteId: string | undefined) { return siteFromList?.name ?? site?.name; } -function useCollectionName(siteId: string | undefined, collectionSlug: string | undefined) { +function useCollectionName( + siteId: string | undefined, + collectionSlug: string | undefined, +) { const { data: collections } = useQuery({ queryKey: ["collections", siteId], queryFn: () => getCollections(siteId as string), enabled: !!siteId && !!collectionSlug, }); - return collections?.find((c) => c.slug === collectionSlug)?.name ?? collectionSlug; + return ( + collections?.find((c) => c.slug === collectionSlug)?.name ?? collectionSlug + ); } -function useSingletonName(siteId: string | undefined, slug: string | undefined) { +function useSingletonName( + siteId: string | undefined, + slug: string | undefined, +) { const { data: collections } = useQuery({ queryKey: ["collections", siteId], queryFn: () => getCollections(siteId as string), enabled: !!siteId && !!slug, }); - return collections?.find((c) => c.is_singleton && c.slug === slug)?.name ?? slug; + return ( + collections?.find((c) => c.is_singleton && c.slug === slug)?.name ?? slug + ); } -function useEntrySlugLabel(siteId: string | undefined, entryId: string | undefined) { +function useEntrySlugLabel( + siteId: string | undefined, + entryId: string | undefined, +) { const { data: entry } = useQuery({ queryKey: ["entry", siteId, entryId], queryFn: () => getEntryById(siteId as string, entryId as string), @@ -107,7 +128,10 @@ function useEntrySlugLabel(siteId: string | undefined, entryId: string | undefin return entry?.slug ?? entryId?.slice(0, 8); } -function useBreadcrumbLabels(defs: BreadcrumbDef[], params: Record) { +function useBreadcrumbLabels( + defs: BreadcrumbDef[], + params: Record, +) { const siteId = params.siteId; const siteName = useSiteName(siteId); const collectionName = useCollectionName(siteId, params.collectionSlug); @@ -131,7 +155,11 @@ function useBreadcrumbLabels(defs: BreadcrumbDef[], params: Record, crumbIndex: number): string | undefined { +function buildHref( + routeId: string, + params: Record, + crumbIndex: number, +): string | undefined { const siteId = params.siteId; if (!siteId) return undefined; @@ -157,7 +185,9 @@ export function AppBreadcrumb() { let config: BreadcrumbConfig | undefined; let routeId: string | undefined; for (let i = matches.length - 1; i >= 0; i--) { - const found = breadcrumbConfigs.find((c) => c.routeId === matches[i].routeId); + const found = breadcrumbConfigs.find( + (c) => c.routeId === matches[i].routeId, + ); if (found) { config = found; routeId = matches[i].routeId; @@ -188,8 +218,10 @@ export function AppBreadcrumb() { return ( - {config.crumbs.map((def, i) => ( - + {config.crumbs.map((def, i) => ( + {i > 0 && } @@ -210,13 +242,17 @@ export function AppBreadcrumb() { const href = buildHref(routeId, params, i); return ( - + {i > 0 && } {isLast ? ( {label} ) : href ? ( - }>{label} + }> + {label} + ) : ( {label} )} @@ -227,4 +263,4 @@ export function AppBreadcrumb() { ); -} \ No newline at end of file +} diff --git a/apps/dashboard/src/components/backups/backups-section.tsx b/apps/dashboard/src/components/backups/backups-section.tsx index 65506932..86a406f4 100644 --- a/apps/dashboard/src/components/backups/backups-section.tsx +++ b/apps/dashboard/src/components/backups/backups-section.tsx @@ -21,7 +21,6 @@ import { CardTitle, } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; -import { Field, FieldLabel } from "@/components/ui/field"; import { Dialog, DialogContent, @@ -30,6 +29,7 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { Field, FieldLabel } from "@/components/ui/field"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { ScrollArea } from "@/components/ui/scroll-area"; @@ -91,13 +91,17 @@ function formatBytes(bytes: number): string { return `${value.toFixed(1)} ${units[i]}`; } -function statusVariant(status: string): "default" | "secondary" | "destructive" { +function statusVariant( + status: string, +): "default" | "secondary" | "destructive" { if (status === "success") return "default"; if (status === "failed") return "destructive"; return "secondary"; } -type RestoreSource = { type: "backup"; backup: BackupInfo } | { type: "upload"; file: File }; +type RestoreSource = + | { type: "backup"; backup: BackupInfo } + | { type: "upload"; file: File }; export function BackupsSection({ scope }: { scope: BackupScope }) { const queryClient = useQueryClient(); @@ -107,8 +111,12 @@ export function BackupsSection({ scope }: { scope: BackupScope }) { const [includeFiles, setIncludeFiles] = useState(true); const [encrypt, setEncrypt] = useState(false); - const [restoreSource, setRestoreSource] = useState(null); - const [restoreMode, setRestoreMode] = useState<"instance" | "site">(isInstance ? "instance" : "site"); + const [restoreSource, setRestoreSource] = useState( + null, + ); + const [restoreMode, setRestoreMode] = useState<"instance" | "site">( + isInstance ? "instance" : "site", + ); const [selectedSiteIds, setSelectedSiteIds] = useState([]); const [importAsNew, setImportAsNew] = useState(false); const [confirmText, setConfirmText] = useState(""); @@ -132,7 +140,8 @@ export function BackupsSection({ scope }: { scope: BackupScope }) { queryClient.invalidateQueries({ queryKey: ["backup-schedules", scopeKey] }); const createMutation = useMutation({ - mutationFn: () => createBackup(scope, { include_files: includeFiles, encrypt }), + mutationFn: () => + createBackup(scope, { include_files: includeFiles, encrypt }), onSuccess: () => { invalidateBackups(); toast.success("Backup created"); @@ -154,11 +163,18 @@ export function BackupsSection({ scope }: { scope: BackupScope }) { if (!restoreSource) return; // Site-settings scope: always restores into the current site (id from URL). if (!isInstance) { - const opts = { mode: "site" as const, import_as_new: importAsNew, confirm: RESTORE_WORD }; + const opts = { + mode: "site" as const, + import_as_new: importAsNew, + confirm: RESTORE_WORD, + }; if (restoreSource.type === "upload") { await restoreBackupUpload(scope, restoreSource.file, opts); } else { - await restoreBackup(scope, { backup_id: restoreSource.backup.id, ...opts }); + await restoreBackup(scope, { + backup_id: restoreSource.backup.id, + ...opts, + }); } return; } @@ -223,13 +239,16 @@ export function BackupsSection({ scope }: { scope: BackupScope }) { } function toggleSite(id: string) { - setSelectedSiteIds((prev) => (prev.includes(id) ? prev.filter((s) => s !== id) : [...prev, id])); + setSelectedSiteIds((prev) => + prev.includes(id) ? prev.filter((s) => s !== id) : [...prev, id], + ); } const backups = backupsQuery.data ?? []; const schedules = schedulesQuery.data ?? []; // For an instance backup picking sites, at least one site must be selected. - const needsSitePick = isInstance && inspect?.scope === "instance" && restoreMode === "site"; + const needsSitePick = + isInstance && inspect?.scope === "instance" && restoreMode === "site"; const confirmReady = confirmText === RESTORE_WORD && !inspecting && @@ -260,10 +279,16 @@ export function BackupsSection({ scope }: { scope: BackupScope }) { checked={includeFiles} onCheckedChange={(v) => setIncludeFiles(Boolean(v))} /> - Include uploaded files + + Include uploaded files + - setEncrypt(Boolean(v))} /> + setEncrypt(Boolean(v))} + /> Encrypt
@@ -283,7 +308,10 @@ export function BackupsSection({ scope }: { scope: BackupScope }) { Restore from file - @@ -302,7 +330,9 @@ export function BackupsSection({ scope }: { scope: BackupScope }) { Created Status Size - Files + + Files + Actions @@ -314,11 +344,17 @@ export function BackupsSection({ scope }: { scope: BackupScope }) {
- {b.status} - {b.encrypted && } + + {b.status} + + {b.encrypted && ( + + )}
- {formatBytes(b.size_bytes)} + + {formatBytes(b.size_bytes)} + {b.includes_files ? b.file_count : "—"} @@ -330,13 +366,19 @@ export function BackupsSection({ scope }: { scope: BackupScope }) { variant="ghost" size="icon" title="Download" - render={} + render={ + + + + } /> @@ -391,14 +433,18 @@ export function BackupsSection({ scope }: { scope: BackupScope }) { /> {/* Restore confirmation */} - !open && closeRestore()}> + !open && closeRestore()} + > Confirm restore - Restoring replaces all data within the chosen scope. This cannot be undone. + Restoring replaces all data within the chosen scope. This cannot + be undone. @@ -409,7 +455,9 @@ export function BackupsSection({ scope }: { scope: BackupScope }) {
)} {isInstance && inspectError && ( -

Could not read this backup: {inspectError}

+

+ Could not read this backup: {inspectError} +

)} {/* A single-site backup: no picker, just name the site being restored. */} @@ -417,7 +465,9 @@ export function BackupsSection({ scope }: { scope: BackupScope }) {

Restores the site{" "} - {inspect.sites[0]?.name ?? inspect.sites[0]?.id ?? "in this backup"} + {inspect.sites[0]?.name ?? + inspect.sites[0]?.id ?? + "in this backup"} .

@@ -427,7 +477,12 @@ export function BackupsSection({ scope }: { scope: BackupScope }) { {isInstance && inspect?.scope === "instance" && (
- + setRestoreMode(v as "instance" | "site") + } + > @@ -441,7 +496,11 @@ export function BackupsSection({ scope }: { scope: BackupScope }) { sites={inspect.sites} selected={selectedSiteIds} onToggle={toggleSite} - onToggleAll={(all) => setSelectedSiteIds(all ? inspect.sites.map((s) => s.id) : [])} + onToggleAll={(all) => + setSelectedSiteIds( + all ? inspect.sites.map((s) => s.id) : [], + ) + } /> )}
@@ -449,14 +508,22 @@ export function BackupsSection({ scope }: { scope: BackupScope }) { {((isInstance && restoreMode === "site") || !isInstance) && ( - setImportAsNew(Boolean(v))} /> - Import as a new site (keep the existing one) + setImportAsNew(Boolean(v))} + /> + + Import as a new site (keep the existing one) + )}
void; }) { if (sites.length === 0) { - return

This backup contains no sites.

; + return ( +

+ This backup contains no sites. +

+ ); } const allSelected = selected.length === sites.length; return (
- - onToggleAll(Boolean(v))} /> - Select all ({selected.length}/{sites.length}) + + onToggleAll(Boolean(v))} + /> + + Select all ({selected.length}/{sites.length}) +
{sites.map((s) => ( - - onToggle(s.id)} /> + + onToggle(s.id)} + /> - {s.name ?? "(unnamed site)"} - {s.id} + + {s.name ?? "(unnamed site)"} + + + {s.id} + @@ -535,7 +627,14 @@ interface SchedulesCardProps { onDelete: (id: string) => Promise; } -function SchedulesCard({ schedules, loading, onCreate, onToggle, onRun, onDelete }: SchedulesCardProps) { +function SchedulesCard({ + schedules, + loading, + onCreate, + onToggle, + onRun, + onDelete, +}: SchedulesCardProps) { const [preset, setPreset] = useState(CRON_PRESETS[0].value); const [customCron, setCustomCron] = useState("0 2 * * *"); const [retention, setRetention] = useState(7); @@ -608,11 +707,19 @@ function SchedulesCard({ schedules, loading, onCreate, onToggle, onRun, onDelete
- setIncludeFiles(Boolean(v))} /> + setIncludeFiles(Boolean(v))} + /> Files - setEncrypt(Boolean(v))} /> + setEncrypt(Boolean(v))} + /> Encrypt
@@ -624,7 +731,9 @@ function SchedulesCard({ schedules, loading, onCreate, onToggle, onRun, onDelete {loading ? ( ) : schedules.length === 0 ? ( -

No schedules configured.

+

+ No schedules configured. +

) : (
{schedules.map((s) => ( @@ -640,19 +749,31 @@ function SchedulesCard({ schedules, loading, onCreate, onToggle, onRun, onDelete

- Keep {s.retention_n} · {s.include_files ? "with files" : "no files"} ·{" "} + Keep {s.retention_n} ·{" "} + {s.include_files ? "with files" : "no files"} ·{" "} {s.encrypt ? "encrypted" : "plaintext"} - {s.next_run_at && ` · next ${new Date(s.next_run_at).toLocaleString()}`} + {s.next_run_at && + ` · next ${new Date(s.next_run_at).toLocaleString()}`}

- -
diff --git a/apps/dashboard/src/components/dashboard-header.tsx b/apps/dashboard/src/components/dashboard-header.tsx index a318d063..35c6f670 100644 --- a/apps/dashboard/src/components/dashboard-header.tsx +++ b/apps/dashboard/src/components/dashboard-header.tsx @@ -54,7 +54,11 @@ export function DashboardHeader() { + - -
+ + )} + + {data && data.total > data.per_page && ( +
+ + {filteredItems.length} of {data.total} files + +
+ +
- )} - +
+ )} + - {/* ---------------------------------------------------------------- + {/* ---------------------------------------------------------------- Upload tab — using a + + + + + + + ); } diff --git a/apps/dashboard/src/components/revisions-panel.tsx b/apps/dashboard/src/components/revisions-panel.tsx index e499cefa..e59a49fe 100644 --- a/apps/dashboard/src/components/revisions-panel.tsx +++ b/apps/dashboard/src/components/revisions-panel.tsx @@ -1,5 +1,6 @@ -import { useCallback, useMemo, useState } from "react"; +import { useForm } from "@tanstack/react-form"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { formatDistanceToNow } from "date-fns"; import { AlertTriangle, ChevronDown, @@ -10,11 +11,10 @@ import { RotateCcw, User, } from "lucide-react"; -import { formatDistanceToNow } from "date-fns"; +import { useCallback, useMemo, useState } from "react"; import { toast } from "sonner"; -import { useForm } from "@tanstack/react-form"; import { z } from "zod"; - +import { DynamicForm } from "@/components/dynamic-form"; import { AlertDialog, AlertDialogAction, @@ -36,10 +36,8 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { cn } from "@/lib/utils"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; -import { Skeleton } from "@/components/ui/skeleton"; import { Sheet, SheetContent, @@ -47,20 +45,21 @@ import { SheetHeader, SheetTitle, } from "@/components/ui/sheet"; +import { Skeleton } from "@/components/ui/skeleton"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; -import { DynamicForm } from "@/components/dynamic-form"; import { - getEntryRevisions, - restoreEntryRevision, type Entry, type EntryRevision, + getEntryRevisions, + restoreEntryRevision, type SchemaDefinition, } from "@/lib/api"; +import { cn } from "@/lib/utils"; // --------------------------------------------------------------------------- // Constants diff --git a/apps/dashboard/src/components/sidebar/site-switcher.tsx b/apps/dashboard/src/components/sidebar/site-switcher.tsx index df252a40..7f293078 100644 --- a/apps/dashboard/src/components/sidebar/site-switcher.tsx +++ b/apps/dashboard/src/components/sidebar/site-switcher.tsx @@ -120,7 +120,11 @@ export function SiteSwitcher({ className="gap-3 px-3 py-2.5" >
- +
{team.name} diff --git a/apps/dashboard/src/components/site-avatar.tsx b/apps/dashboard/src/components/site-avatar.tsx index 02c77904..e782665d 100644 --- a/apps/dashboard/src/components/site-avatar.tsx +++ b/apps/dashboard/src/components/site-avatar.tsx @@ -1,43 +1,43 @@ -import { Hashvatar } from "hashvatar/react"; -import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; -import { cn } from "@/lib/utils"; -import { useState } from "react"; - -type SiteAvatarProps = { - siteName: string; - size?: number; - siteLogo?: string | null; - className?: string | undefined; - animate?: boolean; -}; - -export function SiteAvatar({ - siteName, - size = 32, - siteLogo, - className, - animate, -}: SiteAvatarProps) { - const [isHovered, setIsHovered] = useState(false); - const shouldAnimate = animate !== undefined ? animate : isHovered; - - return ( - setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > - - - - - - ); -} +import { Hashvatar } from "hashvatar/react"; +import { useState } from "react"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { cn } from "@/lib/utils"; + +type SiteAvatarProps = { + siteName: string; + size?: number; + siteLogo?: string | null; + className?: string | undefined; + animate?: boolean; +}; + +export function SiteAvatar({ + siteName, + size = 32, + siteLogo, + className, + animate, +}: SiteAvatarProps) { + const [isHovered, setIsHovered] = useState(false); + const shouldAnimate = animate !== undefined ? animate : isHovered; + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + + + + + ); +} diff --git a/apps/dashboard/src/components/site-settings/members-section.tsx b/apps/dashboard/src/components/site-settings/members-section.tsx index df25a162..2eb70123 100644 --- a/apps/dashboard/src/components/site-settings/members-section.tsx +++ b/apps/dashboard/src/components/site-settings/members-section.tsx @@ -97,7 +97,9 @@ export function MembersSection({ }); const handleAdd = () => { - const user = candidates.find((candidate) => candidate.id === selectedUserId); + const user = candidates.find( + (candidate) => candidate.id === selectedUserId, + ); if (user) inviteMutation.mutate(user.username); }; diff --git a/apps/dashboard/src/components/site-settings/user-combobox.tsx b/apps/dashboard/src/components/site-settings/user-combobox.tsx index 4e3f44d8..ae7358e5 100644 --- a/apps/dashboard/src/components/site-settings/user-combobox.tsx +++ b/apps/dashboard/src/components/site-settings/user-combobox.tsx @@ -48,14 +48,19 @@ export function UserCombobox({ aria-expanded={open} className="w-full justify-between font-normal" > - + {selected ? selected.username : placeholder} } /> - + diff --git a/apps/dashboard/src/components/tiptap-editor.tsx b/apps/dashboard/src/components/tiptap-editor.tsx index 47f6b68f..7abfe117 100644 --- a/apps/dashboard/src/components/tiptap-editor.tsx +++ b/apps/dashboard/src/components/tiptap-editor.tsx @@ -248,7 +248,12 @@ export function TiptapEditor({ > - diff --git a/apps/dashboard/src/contexts/auth-context.tsx b/apps/dashboard/src/contexts/auth-context.tsx index 7cddf374..c21482f7 100644 --- a/apps/dashboard/src/contexts/auth-context.tsx +++ b/apps/dashboard/src/contexts/auth-context.tsx @@ -1,6 +1,6 @@ -import { createContext, type ReactNode, useContext } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { logoutApi, getMe, type UserPublic } from "@/lib/api"; +import { createContext, type ReactNode, useContext } from "react"; +import { getMe, logoutApi, type UserPublic } from "@/lib/api"; interface AuthContextValue { user: UserPublic | null; diff --git a/apps/dashboard/src/lib/api.ts b/apps/dashboard/src/lib/api.ts index 3ace12d3..017356c9 100644 --- a/apps/dashboard/src/lib/api.ts +++ b/apps/dashboard/src/lib/api.ts @@ -5,7 +5,11 @@ export class ApiError extends Error { status: number; body?: Record; - constructor(status: number, message?: string, body?: Record) { + constructor( + status: number, + message?: string, + body?: Record, + ) { super(message); this.status = status; this.body = body; @@ -89,7 +93,9 @@ export function isOperator(role: InstanceRole | null | undefined): boolean { } /** Human label for an instance role, used in settings/user lists. */ -export function instanceRoleLabel(role: InstanceRole | null | undefined): string { +export function instanceRoleLabel( + role: InstanceRole | null | undefined, +): string { if (role === "instance_owner") return "Instance owner"; if (role === "instance_admin") return "Instance admin"; return "User"; @@ -597,7 +603,10 @@ export async function listBackups(scope: BackupScope) { return api(`${backupScopePrefix(scope)}/backups`); } -export async function createBackup(scope: BackupScope, input: CreateBackupInput) { +export async function createBackup( + scope: BackupScope, + input: CreateBackupInput, +) { return api(`${backupScopePrefix(scope)}/backups`, { method: "POST", body: JSON.stringify(input), @@ -611,7 +620,10 @@ export async function deleteBackup(scope: BackupScope, backupId: string) { } /** Same-origin URL for downloading a backup artifact (auth via cookie). */ -export function backupDownloadUrl(scope: BackupScope, backupId: string): string { +export function backupDownloadUrl( + scope: BackupScope, + backupId: string, +): string { return `${BASE_URL}${backupScopePrefix(scope)}/backups/${backupId}/download`; } @@ -625,7 +637,12 @@ export async function restoreBackup(scope: BackupScope, input: RestoreInput) { export async function restoreBackupUpload( scope: BackupScope, file: File, - opts: { mode?: "instance" | "site"; site_id?: string; import_as_new?: boolean; confirm: string }, + opts: { + mode?: "instance" | "site"; + site_id?: string; + import_as_new?: boolean; + confirm: string; + }, ) { const formData = new FormData(); formData.append("file", file); @@ -634,15 +651,22 @@ export async function restoreBackupUpload( formData.append("import_as_new", opts.import_as_new ? "true" : "false"); formData.append("confirm", opts.confirm); const csrfToken = getCsrfToken(); - const res = await fetch(`${BASE_URL}${backupScopePrefix(scope)}/restore/upload`, { - method: "POST", - credentials: "include", - body: formData, - headers: csrfToken ? { "X-CSRF-Token": csrfToken } : undefined, - }); + const res = await fetch( + `${BASE_URL}${backupScopePrefix(scope)}/restore/upload`, + { + method: "POST", + credentials: "include", + body: formData, + headers: csrfToken ? { "X-CSRF-Token": csrfToken } : undefined, + }, + ); if (!res.ok) { const body = await res.json().catch(() => ({})); - throw new ApiError(res.status, body.message || body.error || "Restore failed", body); + throw new ApiError( + res.status, + body.message || body.error || "Restore failed", + body, + ); } } @@ -666,15 +690,22 @@ export async function inspectBackupUpload(scope: BackupScope, file: File) { const formData = new FormData(); formData.append("file", file); const csrfToken = getCsrfToken(); - const res = await fetch(`${BASE_URL}${backupScopePrefix(scope)}/restore/inspect/upload`, { - method: "POST", - credentials: "include", - body: formData, - headers: csrfToken ? { "X-CSRF-Token": csrfToken } : undefined, - }); + const res = await fetch( + `${BASE_URL}${backupScopePrefix(scope)}/restore/inspect/upload`, + { + method: "POST", + credentials: "include", + body: formData, + headers: csrfToken ? { "X-CSRF-Token": csrfToken } : undefined, + }, + ); if (!res.ok) { const body = await res.json().catch(() => ({})); - throw new ApiError(res.status, body.message || body.error || "Could not read backup file", body); + throw new ApiError( + res.status, + body.message || body.error || "Could not read backup file", + body, + ); } return (await res.json()) as InspectResult; } @@ -683,14 +714,21 @@ export async function listBackupSchedules(scope: BackupScope) { return api(`${backupScopePrefix(scope)}/backup-schedules`); } -export async function createBackupSchedule(scope: BackupScope, input: ScheduleInput) { +export async function createBackupSchedule( + scope: BackupScope, + input: ScheduleInput, +) { return api(`${backupScopePrefix(scope)}/backup-schedules`, { method: "POST", body: JSON.stringify(input), }); } -export async function updateBackupSchedule(scope: BackupScope, id: string, input: ScheduleInput) { +export async function updateBackupSchedule( + scope: BackupScope, + id: string, + input: ScheduleInput, +) { return api(`${backupScopePrefix(scope)}/backup-schedules/${id}`, { method: "PUT", body: JSON.stringify(input), @@ -704,9 +742,12 @@ export async function deleteBackupSchedule(scope: BackupScope, id: string) { } export async function runBackupSchedule(scope: BackupScope, id: string) { - return api(`${backupScopePrefix(scope)}/backup-schedules/${id}/run`, { - method: "POST", - }); + return api( + `${backupScopePrefix(scope)}/backup-schedules/${id}/run`, + { + method: "POST", + }, + ); } // --- Sites API --- diff --git a/apps/dashboard/src/routes/_admin/_shell/index.tsx b/apps/dashboard/src/routes/_admin/_shell/index.tsx index f5881c35..e2506d4d 100644 --- a/apps/dashboard/src/routes/_admin/_shell/index.tsx +++ b/apps/dashboard/src/routes/_admin/_shell/index.tsx @@ -9,6 +9,7 @@ import { Cloud, Globe, HardDrive, Plus } from "lucide-react"; import { useEffect, useState } from "react"; import { toast } from "sonner"; import { z } from "zod"; +import { SiteAvatar } from "@/components/site-avatar"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { @@ -45,10 +46,9 @@ import { createSite, getSites, isOperator, - siteRoleLabel, type SiteWithRole, + siteRoleLabel, } from "@/lib/api"; -import { SiteAvatar } from "@/components/site-avatar"; export const Route = createFileRoute("/_admin/_shell/")({ validateSearch: z.object({ diff --git a/apps/dashboard/src/routes/_admin/_shell/settings.tsx b/apps/dashboard/src/routes/_admin/_shell/settings.tsx index 14dfda88..dd269cc6 100644 --- a/apps/dashboard/src/routes/_admin/_shell/settings.tsx +++ b/apps/dashboard/src/routes/_admin/_shell/settings.tsx @@ -42,31 +42,31 @@ function InstanceSettingsLayout() {
- + + } + > + General + + } + > + Users + + {isOwner && ( } + render={} > - General + Backups - } - > - Users - - {isOwner && ( - } - > - Backups - - )} - + )} + diff --git a/apps/dashboard/src/routes/_admin/sites.$siteId.tsx b/apps/dashboard/src/routes/_admin/sites.$siteId.tsx index ecb46e34..b02aaff1 100644 --- a/apps/dashboard/src/routes/_admin/sites.$siteId.tsx +++ b/apps/dashboard/src/routes/_admin/sites.$siteId.tsx @@ -1,13 +1,13 @@ import { createFileRoute, Outlet } from "@tanstack/react-router"; -import { AppSidebar } from "@/components/sidebar/app-sidebar"; import { AppBreadcrumb } from "@/components/app-breadcrumb"; +import { AppSidebar } from "@/components/sidebar/app-sidebar"; +import { ModeToggle } from "@/components/theme-toggle"; import { Separator } from "@/components/ui/separator"; import { SidebarInset, SidebarProvider, SidebarTrigger, } from "@/components/ui/sidebar"; -import { ModeToggle } from "@/components/theme-toggle"; export const Route = createFileRoute("/_admin/sites/$siteId")({ component: SiteLayout, @@ -33,4 +33,4 @@ function SiteLayout() { ); -} \ No newline at end of file +} diff --git a/apps/dashboard/src/routes/_admin/sites.$siteId/collections.tsx b/apps/dashboard/src/routes/_admin/sites.$siteId/collections.tsx index feed21bb..78e4aef7 100644 --- a/apps/dashboard/src/routes/_admin/sites.$siteId/collections.tsx +++ b/apps/dashboard/src/routes/_admin/sites.$siteId/collections.tsx @@ -491,10 +491,7 @@ function SortableFieldItem({ const newOpts = (field.options ?? []).filter( (_, i) => i !== optIdx, ); - form.setFieldValue( - `fields[${index}].options`, - newOpts, - ); + form.setFieldValue(`fields[${index}].options`, newOpts); }} > × diff --git a/apps/dashboard/src/routes/_admin/sites.$siteId/settings.tsx b/apps/dashboard/src/routes/_admin/sites.$siteId/settings.tsx index 44b06595..0ca45444 100644 --- a/apps/dashboard/src/routes/_admin/sites.$siteId/settings.tsx +++ b/apps/dashboard/src/routes/_admin/sites.$siteId/settings.tsx @@ -4,8 +4,8 @@ import { Outlet, useRouterState, } from "@tanstack/react-router"; -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useSiteRole } from "@/components/site-settings/use-site-role"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; export const Route = createFileRoute("/_admin/sites/$siteId/settings")({ component: SettingsLayout, @@ -31,69 +31,66 @@ function SettingsLayout() {
- + + } + > + General + + + } + > + Members + + {canManage && ( + + } + > + API Keys + + )} + {canManage && ( } + render={ + + } > - General + Webhooks + )} + {canManage && ( } > - Members + Backups - {canManage && ( - - } - > - API Keys - - )} - {canManage && ( - - } - > - Webhooks - - )} - {canManage && ( - - } - > - Backups - - )} - + )} + diff --git a/apps/dashboard/src/styles.css b/apps/dashboard/src/styles.css index abcdb6fb..349e876b 100644 --- a/apps/dashboard/src/styles.css +++ b/apps/dashboard/src/styles.css @@ -117,7 +117,7 @@ --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring); --font-heading: var(--font-sans); - --font-sans: 'Geist Variable', sans-serif; + --font-sans: "Geist Variable", sans-serif; --radius-2xl: calc(var(--radius) * 1.8); --radius-3xl: calc(var(--radius) * 2.2); --radius-4xl: calc(var(--radius) * 2.6); @@ -133,4 +133,4 @@ html { @apply font-sans; } -} \ No newline at end of file +} From 235cc61bf18c3afe52eec5206fb1ad59a96b3e18 Mon Sep 17 00:00:00 2001 From: Krishna Santosh <75202541+krishna-santosh@users.noreply.github.com> Date: Sun, 21 Jun 2026 10:02:47 +0530 Subject: [PATCH 35/43] chore: formatting and biome ci fixes --- apps/web/biome.json | 10 +- apps/web/src/app/(home)/_download/page.tsx | 32 +++--- apps/web/src/app/(home)/page.tsx | 2 - apps/web/src/app/docs/docs.css | 4 +- .../src/components/landing/backup-section.tsx | 1 + .../src/components/landing/cta-section.tsx | 1 + .../components/landing/dashboard-section.tsx | 60 ------------ .../components/landing/database-section.tsx | 53 +++++----- .../components/landing/features-section.tsx | 3 +- .../src/components/landing/footer-section.tsx | 57 ----------- .../src/components/landing/hero-section.tsx | 98 ++++++++++--------- .../landing/integrations-section.tsx | 1 + .../web/src/components/landing/navigation.tsx | 1 + .../components/landing/security-section.tsx | 7 +- .../landing/testimonials-section.tsx | 10 +- apps/web/src/components/landing/use-cases.tsx | 1 + 16 files changed, 129 insertions(+), 212 deletions(-) diff --git a/apps/web/biome.json b/apps/web/biome.json index 1d3a073a..a2cf8bc3 100644 --- a/apps/web/biome.json +++ b/apps/web/biome.json @@ -7,7 +7,15 @@ }, "files": { "ignoreUnknown": true, - "includes": ["**", "!node_modules", "!.next", "!dist", "!build", "!.source"] + "includes": [ + "**", + "!node_modules", + "!.next", + "!dist", + "!build", + "!.source", + "!src/components/ui" + ] }, "formatter": { "enabled": true, diff --git a/apps/web/src/app/(home)/_download/page.tsx b/apps/web/src/app/(home)/_download/page.tsx index e6b2d57c..d8686bd7 100644 --- a/apps/web/src/app/(home)/_download/page.tsx +++ b/apps/web/src/app/(home)/_download/page.tsx @@ -2,18 +2,10 @@ import { Icon } from "@iconify-icon/react"; import { Check, Code2, Download } from "lucide-react"; -import Image from "next/image"; import { useState } from "react"; import { FooterSection } from "@/components/landing/footer-section"; import { Navigation } from "@/components/landing/navigation"; import { Button } from "@/components/ui/button"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; type OS = "linux" | "macos" | "windows"; type Architecture = "x86_64" | "aarch64" | "arm64"; @@ -195,12 +187,13 @@ export default function DownloadPage() {
{/* OS Selection */}
- +
{osOptions.map((option) => ( . All downloads are verified with SHA256 checksums. Need help? Check our{" "} - + .

diff --git a/apps/web/src/app/(home)/page.tsx b/apps/web/src/app/(home)/page.tsx index 308aef62..7719e99e 100644 --- a/apps/web/src/app/(home)/page.tsx +++ b/apps/web/src/app/(home)/page.tsx @@ -7,9 +7,7 @@ import { FooterSection } from "@/components/landing/footer-section"; import { HeroSection } from "@/components/landing/hero-section"; import { HowItWorksSection } from "@/components/landing/how-it-works-section"; import { IntegrationsSection } from "@/components/landing/integrations-section"; -import { Navigation } from "@/components/landing/navigation"; import { SecuritySection } from "@/components/landing/security-section"; -import { TestimonialsSection } from "@/components/landing/testimonials-section"; import { UseCasesSection } from "@/components/landing/use-cases"; export default function Home() { diff --git a/apps/web/src/app/docs/docs.css b/apps/web/src/app/docs/docs.css index 488ce0ee..da1cd2f0 100644 --- a/apps/web/src/app/docs/docs.css +++ b/apps/web/src/app/docs/docs.css @@ -7,6 +7,6 @@ html { } html > body[data-scroll-locked] { - margin-right: 0px !important; - --removed-body-scroll-bar-size: 0px !important; + margin-right: 0px; + --removed-body-scroll-bar-size: 0px; } diff --git a/apps/web/src/components/landing/backup-section.tsx b/apps/web/src/components/landing/backup-section.tsx index eca26f21..0b0523bc 100644 --- a/apps/web/src/components/landing/backup-section.tsx +++ b/apps/web/src/components/landing/backup-section.tsx @@ -117,6 +117,7 @@ export function BackupSection() { return (
(null); - const [hasAnimated, setHasAnimated] = useState(false); - - useEffect(() => { - const observer = new IntersectionObserver( - ([entry]) => { - if (entry.isIntersecting && !hasAnimated) { - setHasAnimated(true); - const duration = 2500; - const startTime = performance.now(); - const animate = (currentTime: number) => { - const elapsed = currentTime - startTime; - const progress = Math.min(elapsed / duration, 1); - const eased = 1 - (1 - progress) ** 4; - setCount(Math.floor(eased * end)); - setIsScrambling(progress < 0.8); - if (progress < 1) requestAnimationFrame(animate); - }; - requestAnimationFrame(animate); - } - }, - { threshold: 0.5 }, - ); - if (ref.current) observer.observe(ref.current); - return () => observer.disconnect(); - }, [end, hasAnimated]); - - const displayValue = count.toLocaleString(); - - return ( -
- {prefix} - - {displayValue.split("").map((char, i) => ( - - {char} - - ))} - - {suffix} -
- ); -} - function GridBackground() { const canvasRef = useRef(null); const timeRef = useRef(0); diff --git a/apps/web/src/components/landing/database-section.tsx b/apps/web/src/components/landing/database-section.tsx index 6d4b7e26..d94ae5a2 100644 --- a/apps/web/src/components/landing/database-section.tsx +++ b/apps/web/src/components/landing/database-section.tsx @@ -33,6 +33,20 @@ export function DatabaseSection() { return () => clearInterval(interval); }, []); + const lineConfigs = Array.from({ length: 19 }, (_, i) => ({ + x1: 10 + (i % 5) * 20, + y1: 10 + Math.floor(i / 5) * 25, + x2: 10 + ((i + 1) % 5) * 20, + y2: 10 + Math.floor((i + 1) / 5) * 25, + delay: i * 0.15, + })); + + const dotConfigs = Array.from({ length: 20 }, (_, i) => ({ + left: 10 + (i % 5) * 20, + top: 10 + Math.floor(i / 5) * 25, + delay: i * 0.1, + })); + return (
{/* SVG for connecting lines */} {/* Dots */} - {[...Array(20)].map((_, i) => ( + {dotConfigs.map((dot) => (
))} diff --git a/apps/web/src/components/landing/features-section.tsx b/apps/web/src/components/landing/features-section.tsx index aaa88554..6e3bee12 100644 --- a/apps/web/src/components/landing/features-section.tsx +++ b/apps/web/src/components/landing/features-section.tsx @@ -134,7 +134,7 @@ function ParticleVisualization() { export function FeaturesSection() { const [isVisible, setIsVisible] = useState(false); - const [activeFeature, setActiveFeature] = useState(0); + const [_activeFeature, setActiveFeature] = useState(0); const sectionRef = useRef(null); useEffect(() => { @@ -201,6 +201,7 @@ export function FeaturesSection() { ? "opacity-100 translate-y-0" : "opacity-0 translate-y-12" }`} + role="none" onMouseEnter={() => setActiveFeature(0)} > {/* Left: text content */} diff --git a/apps/web/src/components/landing/footer-section.tsx b/apps/web/src/components/landing/footer-section.tsx index ff435835..a25f9bde 100644 --- a/apps/web/src/components/landing/footer-section.tsx +++ b/apps/web/src/components/landing/footer-section.tsx @@ -2,7 +2,6 @@ import { ArrowUpRight } from "lucide-react"; import Link from "next/link"; -import { useEffect, useRef } from "react"; const footerLinks = { Product: [ @@ -42,62 +41,6 @@ const socialLinks = [ { name: "Youtube", href: "https://youtube.com/@velopulent" }, ]; -function AnimatedWaveCanvas() { - const canvasRef = useRef(null); - - useEffect(() => { - const canvas = canvasRef.current; - if (!canvas) return; - - const ctx = canvas.getContext("2d"); - if (!ctx) return; - - let animationId: number; - let time = 0; - - const resize = () => { - canvas.width = canvas.offsetWidth * window.devicePixelRatio; - canvas.height = canvas.offsetHeight * window.devicePixelRatio; - ctx.scale(window.devicePixelRatio, window.devicePixelRatio); - }; - resize(); - window.addEventListener("resize", resize); - - const animate = () => { - const width = canvas.offsetWidth; - const height = canvas.offsetHeight; - ctx.clearRect(0, 0, width, height); - - ctx.strokeStyle = "rgba(100, 200, 150, 0.3)"; - ctx.lineWidth = 1; - - for (let wave = 0; wave < 3; wave++) { - ctx.beginPath(); - for (let x = 0; x <= width; x += 5) { - const y = - height * 0.5 + - Math.sin(x * 0.01 + time + wave * 0.5) * 30 + - Math.sin(x * 0.02 + time * 1.5 + wave) * 20; - if (x === 0) ctx.moveTo(x, y); - else ctx.lineTo(x, y); - } - ctx.stroke(); - } - - time += 0.02; - animationId = requestAnimationFrame(animate); - }; - animate(); - - return () => { - window.removeEventListener("resize", resize); - cancelAnimationFrame(animationId); - }; - }, []); - - return ; -} - export function FooterSection() { return (
diff --git a/apps/web/src/components/landing/hero-section.tsx b/apps/web/src/components/landing/hero-section.tsx index ffee26f4..c0ae4479 100644 --- a/apps/web/src/components/landing/hero-section.tsx +++ b/apps/web/src/components/landing/hero-section.tsx @@ -4,7 +4,7 @@ import { useEffect, useRef, useState } from "react"; const words = ["website", "blog", "app", "business", "portfolio", "platform"]; -function BlurWord({ word, trigger }: { word: string; trigger: number }) { +function BlurWord({ word }: { word: string }) { const letters = word.split(""); const STAGGER = 45; // ms between each letter const DURATION = 500; // blur+opacity fade duration per letter @@ -59,7 +59,11 @@ function BlurWord({ word, trigger }: { word: string; trigger: number }) { timersRef.current.forEach(clearTimeout); }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [trigger]); + }, [ + letters.map, // stagger each letter + letters.forEach, + GRADIENT_HOLD, + ]); // gradient colours cycling across letter positions const gradientColors = [ @@ -70,43 +74,43 @@ function BlurWord({ word, trigger }: { word: string; trigger: number }) { "#eca8d6", ]; + const hex2rgb = (hex: string): [number, number, number] => { + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + return [r, g, b]; + }; + + const charColors = letters.map((char, i) => { + const colorIndex = + (i / Math.max(letters.length - 1, 1)) * (gradientColors.length - 1); + const lower = Math.floor(colorIndex); + const upper = Math.min(lower + 1, gradientColors.length - 1); + const t = colorIndex - lower; + const [r1, g1, b1] = hex2rgb(gradientColors[lower]); + const [r2, g2, b2] = hex2rgb(gradientColors[upper]); + return { + char, + color: `rgb(${Math.round(r1 + (r2 - r1) * t)},${Math.round(g1 + (g2 - g1) * t)},${Math.round(b1 + (b2 - b1) * t)}`, + }; + }); + return ( <> - {letters.map((char, i) => { - const colorIndex = - (i / Math.max(letters.length - 1, 1)) * (gradientColors.length - 1); - const lower = Math.floor(colorIndex); - const upper = Math.min(lower + 1, gradientColors.length - 1); - const t = colorIndex - lower; - - // lerp hex colours - const hex2rgb = (hex: string) => { - const r = parseInt(hex.slice(1, 3), 16); - const g = parseInt(hex.slice(3, 5), 16); - const b = parseInt(hex.slice(5, 7), 16); - return [r, g, b]; - }; - const [r1, g1, b1] = hex2rgb(gradientColors[lower]); - const [r2, g2, b2] = hex2rgb(gradientColors[upper]); - const r = Math.round(r1 + (r2 - r1) * t); - const g = Math.round(g1 + (g2 - g1) * t); - const b = Math.round(b1 + (b2 - b1) * t); - - return ( - - {char} - - ); - })} + {charColors.map((data, idx) => ( + + {data.char} + + ))} ); } @@ -126,6 +130,11 @@ export function HeroSection() { return () => clearInterval(interval); }, []); + const hLines = Array.from({ length: 8 }, (_, i) => ({ top: 12.5 * (i + 1) })); + const vLines = Array.from({ length: 12 }, (_, i) => ({ + left: 8.33 * (i + 1), + })); + return (
{/* Background video */} @@ -135,7 +144,6 @@ export function HeroSection() { muted loop playsInline - aria-hidden="true" className="w-full h-full object-cover object-center opacity-80" > @@ -147,23 +155,23 @@ export function HeroSection() { {/* Subtle grid lines */}
- {[...Array(8)].map((_, i) => ( + {hLines.map((line) => (
))} - {[...Array(12)].map((_, i) => ( + {vLines.map((line) => (
Build your{" "} - + diff --git a/apps/web/src/components/landing/integrations-section.tsx b/apps/web/src/components/landing/integrations-section.tsx index 6f10fa00..e0d8663a 100644 --- a/apps/web/src/components/landing/integrations-section.tsx +++ b/apps/web/src/components/landing/integrations-section.tsx @@ -97,6 +97,7 @@ export function IntegrationsSection() { {frameworks.map((framework, index) => (
setIsMobileMenuOpen(!isMobileMenuOpen)} className={`z-50 xl:hidden p-2 transition-colors duration-500 ${isScrolled || isMobileMenuOpen ? "text-foreground" : "text-white"}`} aria-label="Toggle menu" diff --git a/apps/web/src/components/landing/security-section.tsx b/apps/web/src/components/landing/security-section.tsx index 156399a9..0b390528 100644 --- a/apps/web/src/components/landing/security-section.tsx +++ b/apps/web/src/components/landing/security-section.tsx @@ -158,9 +158,10 @@ export function SecuritySection() { {/* Feature cards stack */}
{securityFeatures.map((feature, index) => ( -
-
+ ))}
diff --git a/apps/web/src/components/landing/testimonials-section.tsx b/apps/web/src/components/landing/testimonials-section.tsx index b3c45116..2defabdc 100644 --- a/apps/web/src/components/landing/testimonials-section.tsx +++ b/apps/web/src/components/landing/testimonials-section.tsx @@ -41,7 +41,7 @@ const testimonials = [ export function TestimonialsSection() { const [activeIndex, setActiveIndex] = useState(0); const [isVisible, setIsVisible] = useState(false); - const [direction, setDirection] = useState<"left" | "right">("right"); + const [_direction, setDirection] = useState<"left" | "right">("right"); const sectionRef = useRef(null); const [pattern, setPattern] = useState(""); @@ -128,12 +128,14 @@ export function TestimonialsSection() { {/* Navigation arrows */}
- Velopulent CMS Logo
diff --git a/apps/web/src/components/landing/backup-section.tsx b/apps/web/src/components/landing/backup-section.tsx index 0b0523bc..d352007b 100644 --- a/apps/web/src/components/landing/backup-section.tsx +++ b/apps/web/src/components/landing/backup-section.tsx @@ -1,6 +1,7 @@ "use client"; import { Cloud, HardDrive, RotateCcw, Shield } from "lucide-react"; +import Image from "next/image"; import { useEffect, useRef, useState } from "react"; const backupFeatures = [ @@ -70,16 +71,17 @@ export function BackupSection() {
{/* Image — left column */}
- Backup and restore
diff --git a/apps/web/src/components/landing/cta-section.tsx b/apps/web/src/components/landing/cta-section.tsx index 0973d23f..f35cba76 100644 --- a/apps/web/src/components/landing/cta-section.tsx +++ b/apps/web/src/components/landing/cta-section.tsx @@ -3,6 +3,7 @@ import { ArrowRight } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; +import Image from "next/image"; export function CtaSection() { const [isVisible, setIsVisible] = useState(false); @@ -96,13 +97,13 @@ export function CtaSection() { Open source, self-hosted

- {/* Right image */} -
- + Two trees connected by glowing arcs
diff --git a/apps/web/src/components/landing/dashboard-section.tsx b/apps/web/src/components/landing/dashboard-section.tsx index b8904ed4..7ac72e68 100644 --- a/apps/web/src/components/landing/dashboard-section.tsx +++ b/apps/web/src/components/landing/dashboard-section.tsx @@ -1,5 +1,6 @@ "use client"; +import Image from "next/image"; import { useEffect, useRef, useState } from "react"; const dashboardCards = [ @@ -223,15 +224,17 @@ export function DashboardSection() { {/* Organic graph image */}
-
diff --git a/apps/web/src/components/landing/database-section.tsx b/apps/web/src/components/landing/database-section.tsx index d94ae5a2..15dcf321 100644 --- a/apps/web/src/components/landing/database-section.tsx +++ b/apps/web/src/components/landing/database-section.tsx @@ -1,5 +1,6 @@ "use client"; +import Image from "next/image"; import { useEffect, useRef, useState } from "react"; const regions = [ @@ -70,16 +71,17 @@ export function DatabaseSection() {
{/* Image globe — colonne gauche, pleine hauteur */}
- Global network sphere
diff --git a/apps/web/src/components/landing/features-section.tsx b/apps/web/src/components/landing/features-section.tsx index 6e3bee12..b82b217f 100644 --- a/apps/web/src/components/landing/features-section.tsx +++ b/apps/web/src/components/landing/features-section.tsx @@ -1,5 +1,6 @@ "use client"; +import Image from "next/image"; import { useEffect, useRef, useState } from "react"; const features = [ @@ -230,11 +231,12 @@ export function FeaturesSection() { {/* Right: mirrored image, full height */}
- {/* Fade left edge into black */} diff --git a/apps/web/src/components/landing/footer-section.tsx b/apps/web/src/components/landing/footer-section.tsx index a25f9bde..b68f0b2c 100644 --- a/apps/web/src/components/landing/footer-section.tsx +++ b/apps/web/src/components/landing/footer-section.tsx @@ -1,6 +1,7 @@ "use client"; import { ArrowUpRight } from "lucide-react"; +import Image from "next/image"; import Link from "next/link"; const footerLinks = { @@ -46,10 +47,11 @@ export function FooterSection() {