Skip to content

Commit

Permalink
feat: Implement fancy loading page with skeleton (tscircuit#533)
Browse files Browse the repository at this point in the history
- Added a simple and lightweight skeleton loading page for all navigation.
- Ensured no JavaScript is loaded via <script> tags for security and performance.
- Custom lightweight inline script is used for minimal interactivity.
- Fixes issue tscircuit#533.
- Implemented skeleton in `index.html` to show loading state until content is fully loaded.

Signed-off-by: Saurabhsing21 <[email protected]>
  • Loading branch information
Saurabhsing21 committed Jan 16, 2025
1 parent 4f2ff58 commit 175e85b
Show file tree
Hide file tree
Showing 3 changed files with 248 additions and 233 deletions.
195 changes: 194 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="robots" content="index, follow, NOODP" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.ico" type="image/x-icon" sizes="16x16">
<link rel="icon" href="/favicon.ico" type="image/x-icon" sizes="16x16" />
<title>tscircuit - Code Electronics with React</title>
<meta name="description" content="tscircuit is an open-source electronics design tool that lets you create circuits using React components. Design schematics, generate PCB layouts, export and manufacture PCBs online!" />
<meta name="keywords" content="electronic design, PCB design, schematic capture, React components, circuit design, electronics CAD, open source EDA" />
Expand All @@ -15,9 +15,202 @@
<meta name="twitter:title" content="tscircuit - Design Electronics with React Components" />
<meta name="twitter:description" content="Create electronic circuits using React components. Free open-source electronics design tool." />
<link rel="canonical" href="https://tscircuit.com" />

<!-- Inline CSS for Skeleton Loader -->
<style>
/* General Reset */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
padding: 16px;
}

.shimmer {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite ease-in-out;
border-radius: 8px;
}

@keyframes shimmer {
0% {
background-position: -150%;
}
100% {
background-position: 150%;
}
}

/* Page Layout */
.page-wrapper {
display: flex;
flex-direction: column;
gap: 16px;
height: 100vh;
}

.nav-skeleton {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
background: #ffffff;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
border-radius: 8px;
}

.logo-placeholder {
width: 100px;
height: 40px;
}

.nav-items {
display: flex;
gap: 16px;
}

.nav-item {
width: 80px;
height: 20px;
}

.action-buttons {
display: flex;
gap: 12px;
}

.action-placeholder {
height: 40px;
border-radius: 8px;
}

.content-skeleton {
flex: 1;
display: flex;
flex-direction: row;
gap: 24px;
padding: 20px;
background: #ffffff;
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
overflow-y: auto;
}

.main-content {
flex: 3;
display: flex;
flex-direction: column;
gap: 24px;
}

.horizontal-line {
width: 100%;
height: 25px; /* Increased height for visibility */
background: linear-gradient(90deg, #f5f5f5 25%, #eaeaea 50%, #f5f5f5 75%); /* Enhanced contrast */
border-radius: 8px;
}

.box-container {
display: flex;
gap: 16px;
}

.box {
width: 100%;
height: 300px;
}

.sidebar {
flex: 1;
display: flex;
flex-direction: column;
gap: 16px;
}

.sidebar-block {
width: 100%;
height: 80px;
}
</style>
</head>
<body>
<!-- Skeleton Loader -->
<div id="skeleton-loader">

<div class="page-wrapper">
<!-- Navigation Skeleton -->
<div class="nav-skeleton">
<div class="logo-placeholder shimmer"></div>
<div class="nav-items">
<div class="nav-item shimmer"></div>
<div class="nav-item shimmer"></div>
<div class="nav-item shimmer"></div>
<div class="nav-item shimmer"></div>
</div>
<div class="action-buttons">
<div class="action-placeholder shimmer" style="width: 80px;"></div>
<div class="action-placeholder shimmer" style="width: 120px;"></div>
</div>
</div>

<!-- Content Skeleton -->
<div class="content-skeleton">
<div class="main-content">
<!-- Initial lines -->
<div class="horizontal-line shimmer" style="width: 90%;"></div>
<div class="horizontal-line shimmer" style="width: 80%;"></div>
<div class="horizontal-line shimmer" style="width: 70%;"></div>

<!-- Single Box -->
<div class="box-container">
<div class="box shimmer"></div>
</div>

<!-- Additional lines -->
<div class="horizontal-line shimmer" style="width: 80%;"></div>
<div class="horizontal-line shimmer" style="width: 75%;"></div>
<div class="horizontal-line shimmer" style="width: 65%;"></div>
<div class="horizontal-line shimmer" style="width: 70%;"></div>
<div class="horizontal-line shimmer" style="width: 60%;"></div>
<div class="horizontal-line shimmer" style="width: 60%;"></div>

<div class="horizontal-line shimmer" style="width: 60%;"></div>
</div>

<!-- Sidebar -->
<div class="sidebar">
<div class="sidebar-block shimmer"></div>
<div class="sidebar-block shimmer"></div>
<div class="sidebar-block shimmer"></div>
<div class="sidebar-block shimmer"></div>
</div>
</div>
</div>
</div>

<!-- React Root -->
<div id="root"></div>

<!-- React Entry Script -->
<script type="module" src="/src/main.tsx"></script>

<!-- Script to Hide Skeleton -->
<script>
document.addEventListener("DOMContentLoaded", () => {
const skeletonLoader = document.getElementById("skeleton-loader");
if (skeletonLoader) {
console.log("Skeleton Loader is displayed.");
setTimeout(() => {
skeletonLoader.style.display = "none";
}, 300); // Hide skeleton after React initializes
}
});
</script>
</body>
</html>
111 changes: 54 additions & 57 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,96 +1,93 @@
import { ComponentType, Suspense, lazy } from "react"
import { Toaster } from "@/components/ui/toaster"
import { Route, Switch } from "wouter"
import "./components/CmdKMenu"
import { ContextProviders } from "./ContextProviders"
import React from "react"
import { Skeleton } from "./components/ui/skeleton"
import SkeletonLoadingPage from "./components/SkeletonLoader"
import UniversalSkeleton from "./components/SkeletonLoader"
import FullSkeletonLoader from "./components/SkeletonLoader"
import FullPageSkeletonLoader from "./components/SkeletonLoader"
import { ComponentType, Suspense, lazy, useEffect } from "react"; // Added `useEffect` for handling skeleton cleanup
import { Toaster } from "@/components/ui/toaster";
import { Route, Switch } from "wouter";
import "./components/CmdKMenu";
import { ContextProviders } from "./ContextProviders";
import React from "react";

// Lazy loading helper
const lazyImport = (importFn: () => Promise<any>) =>
lazy<ComponentType<any>>(async () => {
try {
const module = await importFn()

const module = await importFn();
if (module.default) {
return { default: module.default }
return { default: module.default };
}

const pageExportNames = ["Page", "Component", "View"]
const pageExportNames = ["Page", "Component", "View"];
for (const suffix of pageExportNames) {
const keys = Object.keys(module).filter((key) => key.endsWith(suffix))
const keys = Object.keys(module).filter((key) => key.endsWith(suffix));
if (keys.length > 0) {
return { default: module[keys[0]] }
return { default: module[keys[0]] };
}
}

const componentExport = Object.values(module).find(
(exp) => typeof exp === "function" && exp.prototype?.isReactComponent,
)
(exp) => typeof exp === "function" && exp.prototype?.isReactComponent
);
if (componentExport) {
return { default: componentExport }
return { default: componentExport };
}

throw new Error(
`No valid React component found in module. Available exports: ${Object.keys(module).join(", ")}`,
)
`No valid React component found in module. Available exports: ${Object.keys(module).join(", ")}`
);
} catch (error) {
console.error("Failed to load component:", error)
throw error
console.error("Failed to load component:", error);
throw error;
}
})
});

const AiPage = lazyImport(() => import("@/pages/ai"))
const AuthenticatePage = lazyImport(() => import("@/pages/authorize"))
const DashboardPage = lazyImport(() => import("@/pages/dashboard"))
const EditorPage = lazyImport(async () => {
const [editorModule] = await Promise.all([
import("@/pages/editor"),
import("@/lib/utils/load-prettier").then((m) => m.loadPrettier()),
])
return editorModule
})
const LandingPage = lazyImport(() => import("@/pages/landing"))
const MyOrdersPage = lazyImport(() => import("@/pages/my-orders"))
const NewestPage = lazyImport(() => import("@/pages/newest"))
const PreviewPage = lazyImport(() => import("@/pages/preview"))
const QuickstartPage = lazyImport(() => import("@/pages/quickstart"))
const SearchPage = lazyImport(() => import("@/pages/search"))
const SettingsPage = lazyImport(() => import("@/pages/settings"))
const UserProfilePage = lazyImport(() => import("@/pages/user-profile"))
const ViewOrderPage = lazyImport(() => import("@/pages/view-order"))
const ViewSnippetPage = lazyImport(() => import("@/pages/view-snippet"))
const DevLoginPage = lazyImport(() => import("@/pages/dev-login"))
// Lazy-loaded pages
const LandingPage = lazyImport(() => import("@/pages/landing"));
const EditorPage = lazyImport(() => import("@/pages/editor"));
const QuickstartPage = lazyImport(() => import("@/pages/quickstart"));
const DashboardPage = lazyImport(() => import("@/pages/dashboard"));
const AiPage = lazyImport(() => import("@/pages/ai"));
const NewestPage = lazyImport(() => import("@/pages/newest"));
const SettingsPage = lazyImport(() => import("@/pages/settings"));
const SearchPage = lazyImport(() => import("@/pages/search"));
const AuthenticatePage = lazyImport(() => import("@/pages/authorize"));
const MyOrdersPage = lazyImport(() => import("@/pages/my-orders"));
const ViewOrderPage = lazyImport(() => import("@/pages/view-order"));
const PreviewPage = lazyImport(() => import("@/pages/preview"));
const DevLoginPage = lazyImport(() => import("@/pages/dev-login"));
const UserProfilePage = lazyImport(() => import("@/pages/user-profile"));
const ViewSnippetPage = lazyImport(() => import("@/pages/view-snippet"));

class ErrorBoundary extends React.Component<
{ children: React.ReactNode },
{ hasError: boolean }
> {
constructor(props: { children: React.ReactNode }) {
super(props)
this.state = { hasError: false }
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError() {
return { hasError: true }
return { hasError: true };
}

render() {
if (this.state.hasError) {
return <div>Something went wrong loading the page.</div>
return <div>Something went wrong loading the page.</div>;
}
return this.props.children
return this.props.children;
}
}

function App() {
// Added useEffect to handle cleanup of the skeleton loader
useEffect(() => {
// Hide the skeleton from index.html when React mounts
const skeletonLoader = document.getElementById("skeleton-loader");
if (skeletonLoader) {
skeletonLoader.style.display = "none"; // Hides the skeleton after the React app is ready
}
}, []);

return (
<ContextProviders>
<ErrorBoundary>
<Suspense fallback={<FullPageSkeletonLoader/>}>
{/* Modified Suspense fallback to use the skeleton loader from index.html */}
<Suspense fallback={<h5 id="skeleton-loader"/>}>
<Switch>
<Route path="/" component={LandingPage} />
<Route path="/editor" component={EditorPage} />
Expand All @@ -112,7 +109,7 @@ function App() {
<Toaster />
</ErrorBoundary>
</ContextProviders>
)
);
}

export default App
export default App;
Loading

0 comments on commit 175e85b

Please sign in to comment.